From 08b3c03241ee12b266fd52dbeb2018f39dbe02ca Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 22 Jan 2025 21:19:02 +0100 Subject: [PATCH] project: Allow running multiple instances of a single language server within a single worktree (#23473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 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 | 1589 +++++++++-------- 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 +++ .../project/src/project_tree/server_tree.rs | 428 +++++ crates/proto/proto/zed.proto | 1 + .../refineable/derive_refineable/Cargo.toml | 2 +- crates/ui_macros/Cargo.toml | 2 +- crates/zed/src/zed/quick_action_bar.rs | 46 +- 29 files changed, 2151 insertions(+), 943 deletions(-) create mode 100644 crates/project/src/project_tree.rs create mode 100644 crates/project/src/project_tree/path_trie.rs create mode 100644 crates/project/src/project_tree/server_tree.rs diff --git a/Cargo.lock b/Cargo.lock index 3db783c120..da82ffa84a 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index e7481808e2..b958452a02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 020a86c9f8..7437546cba 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -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"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 67280765f6..9ffb5f3fb9 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -461,12 +461,14 @@ impl Copilot { .on_notification::(|_, _| { /* 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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9efc74c787..ad13de90b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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::>(); + 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::>() + }); 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>>; - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool; + fn supports_inlay_hints(&self, buffer: &Model, cx: &mut AppContext) -> bool; fn document_highlights( &self, @@ -14056,17 +14058,25 @@ impl SemanticsProvider for Model { })) } - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool { + fn supports_inlay_hints(&self, buffer: &Model, 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( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6f789acce7..f0a9fd90af 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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(), diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 2937e75943..e4c28243a1 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -11,7 +11,7 @@ use multi_buffer::Anchor; pub(crate) fn find_specific_language_server_in_selection( editor: &Editor, - cx: &WindowContext, + cx: &mut WindowContext, filter_language: F, language_server_name: &str, ) -> Option<(Anchor, Arc, LanguageServerId, Model)> @@ -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; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 923dcc24b9..abfda04e7b 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -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, cx: &AppContext) -> bool { + fn supports_inlay_hints(&self, buffer: &Model, cx: &mut AppContext) -> bool { if let Some(buffer) = self.to_base(&buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 6936066192..e605bbb2f2 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -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"] } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8061c7efa9..2769e0fe0b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, + attach_kind: OnceLock, } 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, + ) -> Option> { + 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), + ) -> (WorktreeId, Arc) { + match self { + Attach::InstancePerRoot => root_subproject_path, + Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))), + } + } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application @@ -505,6 +539,19 @@ pub trait LspAdapter: 'static + Send + Sync { fn prepare_initialize_params(&self, original: InitializeParams) -> Result { Ok(original) } + fn attach_kind(&self) -> Attach { + Attach::Shared + } + fn find_project_root( + &self, + + _path: &Path, + _ancestor_depth: usize, + _: &Arc, + ) -> Option> { + // By default all language servers are rooted at the root of the worktree. + Some(Arc::from("".as_ref())) + } } async fn try_fetch_server_binary( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 794ab0784e..51d464d409 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -96,6 +96,7 @@ struct LanguageRegistryState { available_languages: Vec, grammars: HashMap, AvailableGrammar>, lsp_adapters: HashMap>>, + all_lsp_adapters: HashMap>, available_lsp_adapters: HashMap Arc + 'static + Send + Sync>>, loading_languages: HashMap>>>>, @@ -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, ) -> Arc { 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> { + self.state.read().all_lsp_adapters.get(name).cloned() + } + pub fn update_lsp_status( &self, server_name: LanguageServerName, diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 184a06f215..62cfdeedcb 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -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::>() + .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()) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3ef2747642..cdd094a129 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -74,6 +74,22 @@ impl LspAdapter for RustLspAdapter { Self::SERVER_NAME.clone() } + fn find_project_root( + &self, + path: &Path, + ancestor_depth: usize, + delegate: &Arc, + ) -> Option> { + 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, diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 0937b47217..eba4d0e870 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -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 diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e9fa1caac2..51de3bc9ce 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -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>, Task>)>>, output_done_rx: Mutex>, - root_path: PathBuf, - working_dir: PathBuf, server: Arc>>, + workspace_folders: Arc>>, + registered_buffers: Arc>>, } /// 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_capture: Arc>>, server: Option, - root_path: &Path, - working_dir: &Path, code_action_kinds: Option>, 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, + params: InitializeParams, configuration: Arc, cx: &AppContext, ) -> Task>> { - let params = if let Some(params) = initialize_params { - params - } else { - self.default_initialize_params(cx) - }; - cx.spawn(|_| async move { let response = self.request::(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::(¶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::(¶ms).log_err(); + } + } + pub fn set_workspace_folders(&self, folders: BTreeSet) { + 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::(¶ms).log_err(); + } + } + + pub fn workspace_folders(&self) -> impl Deref> + '_ { + 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::(&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::(&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::, Arc::new(Mutex::new(None)), None, - root, - root, None, binary.clone(), cx.clone(), @@ -1319,8 +1421,6 @@ impl FakeLanguageServer { None::, 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 diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index b9fcd8df0e..c0d770c5d5 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -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")?; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5149a818cf..7c2c546e70 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -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 diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c58fcb1351..e69b7b95b0 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -942,9 +942,11 @@ fn language_server_for_buffer( ) -> Result<(Arc, Arc)> { 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")) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ba88b5e485..9e3960c925 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6,6 +6,7 @@ use crate::{ lsp_ext_command, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, + project_tree::{LanguageServerTree, 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, worktree_store: Model, toolchain_store: Model, http_client: Arc, environment: Model, fs: Arc, languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, yarn: Model, pub language_servers: HashMap, buffers_being_formatted: HashSet, @@ -149,7 +152,6 @@ pub struct LocalLspStore { supplementary_language_servers: HashMap)>, prettier_store: Model, - current_lsp_settings: HashMap, next_diagnostic_group_id: usize, diagnostics: HashMap< WorktreeId, @@ -163,7 +165,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, - registered_buffers: HashMap, + lsp_tree: Model, } impl LocalLspStore { @@ -172,26 +174,15 @@ impl LocalLspStore { worktree_handle: &Model, delegate: Arc, adapter: Arc, - cx: &mut ModelContext, - ) { + settings: Arc, + 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>> = Default::default(); + let startup = { let server_name = adapter.name.0.clone(); let delegate = delegate as Arc; 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, - language: LanguageName, - cx: &mut ModelContext, - ) { - 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::>(); - - let desired_language_servers = - settings.customized_language_servers(&available_language_servers); - - let mut enabled_lsp_adapters: Vec> = 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, delegate: Arc, allow_binary_download: bool, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task> { 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::>(); @@ -1012,42 +945,58 @@ impl LocalLspStore { ) -> impl Iterator> { 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 { 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> = 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::>() + }); + + 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, &'a Arc)> { 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, &'a Arc)> { // 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 = 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::>() + }); - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); - - 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> = 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::>() + }); + 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> = 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::>() + }); + 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::>(); + 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::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, - 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, - 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::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 { + 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, - ) -> Option> { - 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, @@ -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, - ) -> Result> { - 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, @@ -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, cx: &mut ModelContext, ) -> 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); } } @@ -3396,17 +3421,8 @@ impl LspStore { cx: &mut ModelContext, ) { let buffer_file = buffer.read(cx).file().cloned(); - let buffer_id = buffer.read(cx).remote_id(); if let Some(local_store) = self.as_local_mut() { - if local_store.registered_buffers.contains_key(&buffer_id) { - if let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - { - if let Some(file_url) = lsp::Url::from_file_path(&abs_path).log_err() { - local_store.unregister_buffer_from_language_servers(buffer, file_url, cx); - } - } - } + local_store.unregister_buffer_from_language_servers(buffer, cx); } buffer.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { @@ -3424,9 +3440,7 @@ impl LspStore { let worktree = file.worktree.clone(); if let Some(local) = self.as_local_mut() { - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, cx); - } + local.register_buffer_with_language_servers(buffer, cx); } Some(worktree.read(cx).id()) } else { @@ -3501,23 +3515,23 @@ impl LspStore { cx, ); } - let buffer = buffer_handle.read(cx); - let language_server = match server { - LanguageServerToQuery::Primary => { - match self - .as_local() - .and_then(|local| local.primary_language_server_for_buffer(buffer, cx)) - { - Some((_, server)) => Some(Arc::clone(server)), - None => return Task::ready(Ok(Default::default())), - } - } + + let Some(language_server) = buffer_handle.update(cx, |buffer, cx| match server { + LanguageServerToQuery::Primary => self + .as_local() + .and_then(|local| local.primary_language_server_for_buffer(buffer, cx)) + .map(|(_, server)| server.clone()), LanguageServerToQuery::Other(id) => self .language_server_for_local_buffer(buffer, id, cx) .map(|(_, server)| Arc::clone(server)), + }) else { + return Task::ready(Ok(Default::default())); }; + + let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let (Some(file), Some(language_server)) = (file, language_server) { + + if let Some(file) = file { let lsp_params = match request.to_lsp_params_or_response( &file.abs_path(cx), buffer, @@ -3526,6 +3540,7 @@ impl LspStore { ) { Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params, Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)), + Err(err) => { let message = format!( "{} via {} failed: {}", @@ -3537,6 +3552,7 @@ impl LspStore { return Task::ready(Err(anyhow!(message))); } }; + let status = request.status(); if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); @@ -3611,25 +3627,13 @@ impl LspStore { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let mut language_servers_to_start = Vec::new(); let mut language_formatters_to_check = Vec::new(); for buffer in self.buffer_store.read(cx).buffers() { let buffer = buffer.read(cx); let buffer_file = File::from_dyn(buffer.file()); let buffer_language = buffer.language(); let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - if let Some(language) = buffer_language { - if settings.enable_language_server - && self - .as_local() - .unwrap() - .registered_buffers - .contains_key(&buffer.remote_id()) - { - if let Some(file) = buffer_file { - language_servers_to_start.push((file.worktree.clone(), language.name())); - } - } + if buffer_language.is_some() { language_formatters_to_check.push(( buffer_file.map(|f| f.worktree_id(cx)), settings.into_owned(), @@ -3637,80 +3641,66 @@ impl LspStore { } } - let mut language_servers_to_stop = Vec::new(); - let mut language_servers_to_restart = Vec::new(); - let languages = self.languages.to_vec(); - - let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone(); - let Some(current_lsp_settings) = self.swap_current_lsp_settings(new_lsp_settings.clone()) - else { - return; - }; - for (worktree_id, started_lsp_name) in - self.as_local().unwrap().language_server_ids.keys().cloned() - { - let language = languages.iter().find_map(|l| { - let adapter = self - .languages - .lsp_adapters(&l.name()) - .iter() - .find(|adapter| adapter.name == started_lsp_name)? - .clone(); - Some((l, adapter)) - }); - if let Some((language, adapter)) = language { - let worktree = self.worktree_for_id(worktree_id, cx).ok(); - let root_file = worktree.as_ref().and_then(|worktree| { - worktree - .update(cx, |tree, cx| tree.root_file(cx)) - .map(|f| f as _) - }); - let settings = language_settings(Some(language.name()), root_file.as_ref(), cx); - if !settings.enable_language_server { - language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); - } else if let Some(worktree) = worktree { - let server_name = &adapter.name; - match ( - current_lsp_settings.get(server_name), - new_lsp_settings.get(server_name), - ) { - (None, None) => {} - (Some(_), None) | (None, Some(_)) => { - language_servers_to_restart.push((worktree, language.name())); - } - (Some(current_lsp_settings), Some(new_lsp_settings)) => { - if current_lsp_settings != new_lsp_settings { - language_servers_to_restart.push((worktree, language.name())); - } - } + let mut to_stop = Vec::new(); + if let Some(local) = self.as_local_mut() { + local.lsp_tree.clone().update(cx, |this, cx| { + let mut get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut AppContext| -> Option> { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) } - } - } - } + }; - for (worktree_id, adapter_name) in language_servers_to_stop { - self.stop_local_language_server(worktree_id, adapter_name, cx) - .detach(); + this.on_settings_changed( + &mut get_adapter, + &mut |disposition, cx| { + let worktree = local + .worktree_store + .read(cx) + .worktree_for_id(disposition.path.worktree_id, cx) + .expect("Worktree ID to be valid"); + let delegate = + LocalLspAdapterDelegate::from_local_lsp(local, &worktree, cx); + let adapter = local + .languages + .adapter_for_name(disposition.server_name) + .expect("Adapter to be available"); + local.start_language_server( + &worktree, + delegate, + adapter, + disposition.settings, + cx, + ) + }, + &mut |id| to_stop.push(id), + cx, + ); + }); + } + for id in to_stop { + self.stop_local_language_server(id, cx).detach(); } - if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { prettier_store.on_settings_changed(language_formatters_to_check, cx) }) } - // Start all the newly-enabled language servers. - for (worktree, language) in language_servers_to_start { - self.as_local_mut() - .unwrap() - .start_language_servers(&worktree, language, cx); - } - - // Restart all language servers with changed initialization options. - for (worktree, language) in language_servers_to_restart { - self.restart_local_language_servers(worktree, language, cx); - } - cx.notify(); } @@ -3742,12 +3732,10 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let buffer = buffer_handle.read(cx); - let (lsp_adapter, lang_server) = if let Some((adapter, server)) = + let Some((lsp_adapter, lang_server)) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, action.server_id, cx) - { - (adapter.clone(), server.clone()) - } else { + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) else { return Task::ready(Ok(Default::default())); }; cx.spawn(move |this, mut cx| async move { @@ -3828,19 +3816,16 @@ impl LspStore { } }) } else { - let buffer = buffer_handle.read(cx); - let (_, lang_server) = if let Some((adapter, server)) = + let Some(lang_server) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, server_id, cx) - { - (adapter.clone(), server.clone()) - } else { + .map(|(_, server)| server.clone()) + }) else { return Task::ready(Ok(hint)); }; if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { return Task::ready(Ok(hint)); } - - let buffer_snapshot = buffer.snapshot(); + let buffer_snapshot = buffer_handle.read(cx).snapshot(); cx.spawn(move |_, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), @@ -3873,22 +3858,24 @@ impl LspStore { let Some(server_id) = self .as_local() .and_then(|local| { - local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| { - server - .capabilities() - .linked_editing_range_provider - .is_some() - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) - .next() + buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(_, server)| { + server + .capabilities() + .linked_editing_range_provider + .is_some() + }) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) + .next() + }) }) .or_else(|| { self.upstream_client() @@ -4142,17 +4129,19 @@ impl LspStore { let scope = snapshot.language_scope_at(offset); let language = snapshot.language().cloned(); - let server_ids: Vec<_> = local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect(); + let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect() + }); let buffer = buffer.clone(); cx.spawn(move |this, mut cx| async move { @@ -4472,10 +4461,9 @@ impl LspStore { push_to_history: bool, cx: &mut ModelContext, ) -> Task>> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - if let Some((client, project_id)) = self.upstream_client() { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); cx.spawn(move |_, mut cx| async move { let request = { let completion = completions.borrow()[completion_index].clone(); @@ -4514,9 +4502,14 @@ impl LspStore { }) } else { let server_id = completions.borrow()[completion_index].server_id; - let server = match self.language_server_for_local_buffer(buffer, server_id, cx) { - Some((_, server)) => server.clone(), - _ => return Task::ready(Ok(None)), + let Some(server) = buffer_handle.update(cx, |buffer, cx| { + Some( + self.language_server_for_local_buffer(buffer, server_id, cx)? + .1 + .clone(), + ) + }) else { + return Task::ready(Ok(None)); }; let snapshot = buffer_handle.read(&cx).snapshot(); @@ -4807,6 +4800,7 @@ impl LspStore { }) } else if let Some(local) = self.as_local() { struct WorkspaceSymbolsResult { + server_id: LanguageServerId, lsp_adapter: Arc, worktree: WeakModel, worktree_abs_path: Arc, @@ -4814,7 +4808,8 @@ impl LspStore { } let mut requests = Vec::new(); - for ((worktree_id, _), server_id) in local.language_server_ids.iter() { + let mut requested_servers = BTreeSet::new(); + 'next_server: for ((worktree_id, _), server_ids) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store .read(cx) @@ -4826,55 +4821,63 @@ impl LspStore { if !worktree.is_visible() { continue; } - let worktree_abs_path = worktree.abs_path().clone(); - let (lsp_adapter, server) = match local.language_servers.get(server_id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), + let mut servers_to_query = server_ids + .difference(&requested_servers) + .cloned() + .collect::>(); + for server_id in &servers_to_query { + let (lsp_adapter, server) = match local.language_servers.get(server_id) { + Some(LanguageServerState::Running { + adapter, server, .. + }) => (adapter.clone(), server), - _ => continue, - }; - - requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .log_err() - .map(move |response| { - let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { + _ => continue 'next_server, + }; + let worktree_abs_path = worktree.abs_path().clone(); + let worktree_handle = worktree_handle.clone(); + let server_id = server.server_id(); + requests.push( + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, + ) + .log_err() + .map(move |response| { + let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { + lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { + flat_responses.into_iter().map(|lsp_symbol| { (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { - let location = match lsp_symbol.location { - OneOf::Left(location) => location, - OneOf::Right(_) => { - log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None - } - }; - Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() - } - }).unwrap_or_default(); + }).collect::>() + } + lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { + nested_responses.into_iter().filter_map(|lsp_symbol| { + let location = match lsp_symbol.location { + OneOf::Left(location) => location, + OneOf::Right(_) => { + log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); + return None + } + }; + Some((lsp_symbol.name, lsp_symbol.kind, location)) + }).collect::>() + } + }).unwrap_or_default(); - WorkspaceSymbolsResult { - lsp_adapter, - - worktree: worktree_handle.downgrade(), - worktree_abs_path, - lsp_symbols, - } - }), - ); + WorkspaceSymbolsResult { + server_id, + lsp_adapter, + worktree: worktree_handle.downgrade(), + worktree_abs_path, + lsp_symbols, + } + }), + ); + } + requested_servers.append(&mut servers_to_query); } cx.spawn(move |this, mut cx| async move { @@ -4914,6 +4917,7 @@ impl LspStore { }; let signature = this.symbol_signature(&project_path); Some(CoreSymbol { + source_language_server_id: result.server_id, language_server_name: result.lsp_adapter.name.clone(), source_worktree_id, path: project_path, @@ -4993,19 +4997,19 @@ impl LspStore { buffer: Model, cx: &mut ModelContext, ) -> Option<()> { + let language_servers: Vec<_> = buffer.update(cx, |buffer, cx| { + Some( + self.as_local()? + .language_servers_for_buffer(buffer, cx) + .map(|i| i.1.clone()) + .collect(), + ) + })?; let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); let uri = lsp::Url::from_file_path(abs_path).unwrap(); let next_snapshot = buffer.text_snapshot(); - - let language_servers: Vec<_> = self - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|i| i.1.clone()) - .collect(); - for language_server in language_servers { let language_server = language_server.clone(); @@ -5122,7 +5126,10 @@ impl LspStore { } } - for language_server_id in local.language_server_ids_for_buffer(buffer.read(cx), cx) { + let language_servers = buffer.update(cx, |buffer, cx| { + local.language_server_ids_for_buffer(buffer, cx) + }); + for language_server_id in language_servers { self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx); } @@ -5142,31 +5149,37 @@ impl LspStore { local .language_server_ids .iter() - .filter_map(|((worktree_id, _), server_id)| { + .flat_map(|((worktree_id, _), server_ids)| { let worktree = this .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx)?; - let state = local.language_servers.get(server_id)?; - let delegate = LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_model(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ); - match state { - LanguageServerState::Starting(_) => None, - LanguageServerState::Running { - adapter, server, .. - } => Some(( - adapter.adapter.clone(), - server.clone(), - delegate as Arc, - )), - } + .worktree_for_id(*worktree_id, cx); + let delegate = worktree.map(|worktree| { + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + cx.weak_model(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + }); + + server_ids.iter().filter_map(move |server_id| { + let states = local.language_servers.get(server_id)?; + + match states { + LanguageServerState::Starting { .. } => None, + LanguageServerState::Running { + adapter, server, .. + } => Some(( + adapter.adapter.clone(), + server.clone(), + delegate.clone()? as Arc, + )), + } + }) }) .collect::>() }) @@ -5224,27 +5237,31 @@ impl LspStore { pub(crate) fn language_servers_for_local_buffer<'a>( &'a self, - buffer: &'a Buffer, - cx: &'a AppContext, + buffer: &Buffer, + cx: &mut AppContext, ) -> impl Iterator, &'a Arc)> { - self.as_local().into_iter().flat_map(|local| { - local - .language_server_ids_for_buffer(buffer, cx) - .into_iter() - .filter_map(|server_id| match local.language_servers.get(&server_id)? { + let local = self.as_local(); + let language_server_ids = local + .map(|local| local.language_server_ids_for_buffer(buffer, cx)) + .unwrap_or_default(); + + language_server_ids + .into_iter() + .filter_map( + move |server_id| match local?.language_servers.get(&server_id)? { LanguageServerState::Running { adapter, server, .. } => Some((adapter, server)), _ => None, - }) - }) + }, + ) } pub fn language_server_for_local_buffer<'a>( &'a self, buffer: &'a Buffer, server_id: LanguageServerId, - cx: &'a AppContext, + cx: &'a mut AppContext, ) -> Option<(&'a Arc, &'a Arc)> { self.as_local()? .language_servers_for_buffer(buffer, cx) @@ -5253,39 +5270,11 @@ impl LspStore { fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { self.diagnostic_summaries.remove(&id_to_remove); - let to_remove = Vec::new(); if let Some(local) = self.as_local_mut() { - local.diagnostics.remove(&id_to_remove); - local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }); - - let mut servers_to_remove = HashMap::default(); - let mut servers_to_preserve = HashSet::default(); - for ((worktree_id, server_name), &server_id) in &local.language_server_ids { - if worktree_id == &id_to_remove { - servers_to_remove.insert(server_id, server_name.clone()); - } else { - servers_to_preserve.insert(server_id); - } + let to_remove = local.remove_worktree(id_to_remove, cx); + for server in to_remove { + self.language_server_statuses.remove(&server); } - servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); - for (server_id_to_remove, server_name) in servers_to_remove { - local - .language_server_ids - .remove(&(id_to_remove, server_name)); - local - .language_server_watched_paths - .remove(&server_id_to_remove); - local - .last_workspace_edits_by_language_server - .remove(&server_id_to_remove); - local.language_servers.remove(&server_id_to_remove); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove)); - } - } - for server in to_remove { - self.language_server_statuses.remove(&server); } } @@ -5353,7 +5342,9 @@ impl LspStore { self.as_local_mut() .unwrap() .language_server_ids - .insert((worktree_id, language_server_name), language_server_id); + .entry((worktree_id, language_server_name)) + .or_default() + .insert(language_server_id); } pub fn update_diagnostic_entries( @@ -5489,10 +5480,17 @@ impl LspStore { .await }) } else if let Some(local) = self.as_local() { - let Some(&language_server_id) = local.language_server_ids.get(&( - symbol.source_worktree_id, - symbol.language_server_name.clone(), - )) else { + let Some(language_server_id) = local + .language_server_ids + .get(&( + symbol.source_worktree_id, + symbol.language_server_name.clone(), + )) + .and_then(|ids| { + ids.contains(&symbol.source_language_server_id) + .then_some(symbol.source_language_server_id) + }) + else { return Task::ready(Err(anyhow!( "language server for worktree and language not found" ))); @@ -5633,16 +5631,19 @@ impl LspStore { let snapshot = buffer.read(cx).snapshot(); let scope = position.and_then(|position| snapshot.language_scope_at(position)); - let server_ids = local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect::>(); + + let server_ids = buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect::>() + }); let mut response_results = server_ids .into_iter() @@ -6386,18 +6387,13 @@ impl LspStore { } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - if let Some(local_lsp_store) = self.as_local() { - if let Some(LanguageServerState::Running { server, .. }) = - local_lsp_store.language_servers.get(&id) - { - Some(server.clone()) - } else if let Some((_, server)) = - local_lsp_store.supplementary_language_servers.get(&id) - { - Some(Arc::clone(server)) - } else { - None - } + let local_lsp_store = self.as_local()?; + if let Some(LanguageServerState::Running { server, .. }) = + local_lsp_store.language_servers.get(&id) + { + Some(server.clone()) + } else if let Some((_, server)) = local_lsp_store.supplementary_language_servers.get(&id) { + Some(Arc::clone(server)) } else { None } @@ -6794,6 +6790,7 @@ impl LspStore { &Symbol { language_server_name: symbol.language_server_name, source_worktree_id: symbol.source_worktree_id, + source_language_server_id: symbol.source_language_server_id, path: symbol.path, name: symbol.name, kind: symbol.kind, @@ -7119,14 +7116,14 @@ impl LspStore { cx: AsyncAppContext, ) { let server = match server_state { - Some(LanguageServerState::Starting(task)) => { + Some(LanguageServerState::Starting { startup, .. }) => { let mut timer = cx .background_executor() .timer(SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT) .fuse(); select! { - server = task.fuse() => server, + server = startup.fuse() => server, _ = timer => { log::info!( "timeout waiting for language server {} to finish launching before stopping", @@ -7153,36 +7150,43 @@ impl LspStore { // for the stopped server fn stop_local_language_server( &mut self, - worktree_id: WorktreeId, - adapter_name: LanguageServerName, + server_id: LanguageServerId, cx: &mut ModelContext, ) -> Task> { - let key = (worktree_id, adapter_name); let local = match &mut self.mode { LspStoreMode::Local(local) => local, _ => { return Task::ready(Vec::new()); } }; - let Some(server_id) = local.language_server_ids.remove(&key) else { + + let mut orphaned_worktrees = vec![]; + // Did any of the worktrees reference this server ID at least once? + let mut was_referenced = false; + // Remove this server ID from all entries in the given worktree. + local.language_server_ids.retain(|(worktree, _), ids| { + if !ids.remove(&server_id) { + return true; + } + + was_referenced = true; + if ids.is_empty() { + orphaned_worktrees.push(*worktree); + false + } else { + true + } + }); + let Some(status) = self + .language_server_statuses + .remove(&server_id) + .filter(|_| was_referenced) + else { return Task::ready(Vec::new()); }; - let name = key.1; - log::info!("stopping language server {name}"); - // Remove other entries for this language server as well - let mut orphaned_worktrees = vec![worktree_id]; - let other_keys = local - .language_server_ids - .keys() - .cloned() - .collect::>(); - for other_key in other_keys { - if local.language_server_ids.get(&other_key) == Some(&server_id) { - local.language_server_ids.remove(&other_key); - orphaned_worktrees.push(other_key.0); - } - } + let name = LanguageServerName(status.name.into()); + log::info!("stopping language server {name}"); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { @@ -7217,7 +7221,6 @@ impl LspStore { }); } - self.language_server_statuses.remove(&server_id); let local = self.as_local_mut().unwrap(); for diagnostics in local.diagnostics.values_mut() { diagnostics.retain(|_, diagnostics_by_server_id| { @@ -7256,81 +7259,89 @@ impl LspStore { .spawn(request) .detach_and_log_err(cx); } else { - let language_server_lookup_info: HashSet<(Model, LanguageName)> = buffers + let Some(local) = self.as_local_mut() else { + return; + }; + let language_servers_for_worktrees = buffers .into_iter() .filter_map(|buffer| { - let buffer = buffer.read(cx); - let file = buffer.file()?; - let worktree = File::from_dyn(Some(file))?.worktree.clone(); - let language = - self.languages - .language_for_file(file, Some(buffer.as_rope()), cx)?; - - Some((worktree, language.name())) + buffer.update(cx, |buffer, cx| { + let worktree_id = buffer.file()?.worktree_id(cx); + let language_server_ids = local.language_servers_for_buffer(buffer, cx); + Some(( + language_server_ids + .filter_map(|(adapter, server)| { + let new_adapter = + local.languages.adapter_for_name(&adapter.name()); + new_adapter.map(|adapter| (server.server_id(), adapter)) + }) + .collect::>(), + worktree_id, + )) + }) }) - .collect(); + .fold( + HashMap::default(), + |mut worktree_to_ids, (server_ids, worktree_id)| { + worktree_to_ids + .entry(worktree_id) + .or_insert_with(BTreeMap::new) + .extend(server_ids); + worktree_to_ids + }, + ); - for (worktree, language) in language_server_lookup_info { - self.restart_local_language_servers(worktree, language, cx); - } - } - } + // Multiple worktrees might refer to the same language server; + // we don't want to restart them multiple times. + let mut restarted_language_servers = BTreeMap::new(); + let mut servers_to_stop = BTreeSet::new(); + local.lsp_tree.clone().update(cx, |tree, cx| { + for (worktree_id, adapters_to_restart) in language_servers_for_worktrees { + let Some(worktree_handle) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; + let delegate = + LocalLspAdapterDelegate::from_local_lsp(local, &worktree_handle, cx); + let servers_to_restart = adapters_to_restart.keys().copied().collect(); + tree.restart_language_servers( + worktree_id, + servers_to_restart, + &mut |old_server_id, disposition| match restarted_language_servers + .entry(old_server_id) + { + btree_map::Entry::Vacant(unfilled) => { + servers_to_stop.insert(old_server_id); - fn restart_local_language_servers( - &mut self, - worktree: Model, - language: LanguageName, - cx: &mut ModelContext, - ) { - let worktree_id = worktree.read(cx).id(); - - let stop_tasks = self - .languages - .clone() - .lsp_adapters(&language) - .iter() - .map(|adapter| { - let stop_task = - self.stop_local_language_server(worktree_id, adapter.name.clone(), cx); - (stop_task, adapter.name.clone()) - }) - .collect::>(); - if stop_tasks.is_empty() { - return; - } - - cx.spawn(move |this, mut cx| async move { - // For each stopped language server, record all of the worktrees with which - // it was associated. - let mut affected_worktrees = Vec::new(); - for (stop_task, language_server_name) in stop_tasks { - for affected_worktree_id in stop_task.await { - affected_worktrees.push((affected_worktree_id, language_server_name.clone())); + let adapter = adapters_to_restart + .get(&old_server_id) + .map(Clone::clone) + .expect("Language server adapter to be found"); + let new_id = local.start_language_server( + &worktree_handle, + delegate.clone(), + adapter, + disposition.settings, + cx, + ); + unfilled.insert(new_id); + new_id + } + btree_map::Entry::Occupied(server_id) => *server_id.get(), + }, + ); } + }); + for server_id in servers_to_stop { + self.stop_local_language_server(server_id, cx).detach(); } - - this.update(&mut cx, |this, cx| { - let local = this.as_local_mut().unwrap(); - // Restart the language server for the given worktree. - // - local.start_language_servers(&worktree, language.clone(), cx); - - // Lookup new server ids and set them for each of the orphaned worktrees - for (affected_worktree_id, language_server_name) in affected_worktrees { - if let Some(new_server_id) = local - .language_server_ids - .get(&(worktree_id, language_server_name.clone())) - .cloned() - { - local - .language_server_ids - .insert((affected_worktree_id, language_server_name), new_server_id); - } - } - }) - .ok(); - }) - .detach(); + // for (worktree, language) in language_server_lookup_info { + // self.restart_local_language_servers(worktree, language, cx); + // } + } } pub fn update_diagnostics( @@ -7458,12 +7469,14 @@ impl LspStore { Ok(()) } + #[allow(clippy::too_many_arguments)] fn insert_newly_running_language_server( &mut self, adapter: Arc, language_server: Arc, server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), + workspace_folders: Arc>>, cx: &mut ModelContext, ) { let Some(local) = self.as_local_mut() else { @@ -7474,7 +7487,7 @@ impl LspStore { if local .language_server_ids .get(&key) - .map(|id| id != &server_id) + .map(|ids| !ids.contains(&server_id)) .unwrap_or(false) { return; @@ -7484,11 +7497,12 @@ impl LspStore { // indicating that the server is up and running and ready local.language_servers.insert( server_id, - LanguageServerState::Running { - adapter: adapter.clone(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, + LanguageServerState::running( + workspace_folders.lock().clone(), + adapter.clone(), + language_server.clone(), + None, + ), ); if let Some(file_ops_caps) = language_server .capabilities() @@ -7568,36 +7582,29 @@ impl LspStore { let local = self.as_local_mut().unwrap(); - if local.registered_buffers.contains_key(&buffer.remote_id()) { - let versions = local - .buffer_snapshots - .entry(buffer.remote_id()) - .or_default() - .entry(server_id) - .or_insert_with(|| { - vec![LspBufferSnapshot { - version: 0, - snapshot: buffer.text_snapshot(), - }] - }); + let versions = local + .buffer_snapshots + .entry(buffer.remote_id()) + .or_default() + .entry(server_id) + .or_insert_with(|| { + vec![LspBufferSnapshot { + version: 0, + snapshot: buffer.text_snapshot(), + }] + }); - let snapshot = versions.last().unwrap(); - let version = snapshot.version; - let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - language_server - .notify::( - &lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - adapter.language_id(&language.name()), - version, - initial_snapshot.text(), - ), - }, - ) - .log_err(); - } + let snapshot = versions.last().unwrap(); + let version = snapshot.version; + let initial_snapshot = &snapshot.snapshot; + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + language_server.register_buffer( + buffer.remote_id(), + uri, + adapter.language_id(&language.name()), + version, + initial_snapshot.text(), + ); buffer_handle.update(cx, |buffer, cx| { buffer.set_completion_triggers( @@ -7660,12 +7667,11 @@ impl LspStore { let servers = buffers .into_iter() .flat_map(|buffer| { - local - .language_server_ids_for_buffer(buffer.read(cx), cx) - .into_iter() + buffer.update(cx, |buffer, cx| { + local.language_server_ids_for_buffer(buffer, cx).into_iter() + }) }) .collect::>(); - for server_id in servers { self.cancel_language_server_work(server_id, None, cx); } @@ -7698,16 +7704,6 @@ impl LspStore { ) .ok(); } - - if progress.is_cancellable { - server - .notify::( - &WorkDoneProgressCancelParams { - token: lsp::NumberOrString::String(token.clone()), - }, - ) - .ok(); - } } } } else if let Some((client, project_id)) = self.upstream_client() { @@ -7754,7 +7750,7 @@ impl LspStore { } } - pub fn supplementary_language_servers( + pub(crate) fn supplementary_language_servers( &self, ) -> impl '_ + Iterator { self.as_local().into_iter().flat_map(|local| { @@ -7797,8 +7793,10 @@ impl LspStore { let mut language_server_ids = local .language_server_ids .iter() - .filter_map(|((server_worktree_id, _), server_id)| { - (*server_worktree_id == worktree_id).then_some(*server_id) + .flat_map(|((server_worktree, _), server_ids)| { + server_ids + .iter() + .filter_map(|server_id| server_worktree.eq(&worktree_id).then(|| *server_id)) }) .collect::>(); language_server_ids.sort(); @@ -7859,6 +7857,7 @@ impl LspStore { proto::Symbol { language_server_name: symbol.language_server_name.0.to_string(), source_worktree_id: symbol.source_worktree_id.to_proto(), + language_server_id: symbol.source_language_server_id.to_proto(), worktree_id: symbol.path.worktree_id.to_proto(), path: symbol.path.path.to_string_lossy().to_string(), name: symbol.name.clone(), @@ -7893,6 +7892,9 @@ impl LspStore { Ok(CoreSymbol { language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), source_worktree_id, + source_language_server_id: LanguageServerId::from_proto( + serialized_symbol.language_server_id, + ), path, name: serialized_symbol.name, range: Unclipped(PointUtf16::new(start.row, start.column)) @@ -8289,7 +8291,11 @@ impl LanguageServerLogType { } pub enum LanguageServerState { - Starting(Task>>), + Starting { + startup: Task>>, + /// List of language servers that will be added to the workspace once it's initialization completes. + pending_workspace_folders: Arc>>, + }, Running { adapter: Arc, @@ -8298,10 +8304,50 @@ pub enum LanguageServerState { }, } +impl LanguageServerState { + fn add_workspace_folder(&self, uri: Url) { + match self { + LanguageServerState::Starting { + pending_workspace_folders, + .. + } => { + pending_workspace_folders.lock().insert(uri); + } + LanguageServerState::Running { server, .. } => { + server.add_workspace_folder(uri); + } + } + } + fn _remove_workspace_folder(&self, uri: Url) { + match self { + LanguageServerState::Starting { + pending_workspace_folders, + .. + } => { + pending_workspace_folders.lock().remove(&uri); + } + LanguageServerState::Running { server, .. } => server.remove_workspace_folder(uri), + } + } + fn running( + workspace_folders: BTreeSet, + adapter: Arc, + server: Arc, + simulate_disk_based_diagnostics_completion: Option>, + ) -> Self { + server.set_workspace_folders(workspace_folders); + Self::Running { + adapter, + server, + simulate_disk_based_diagnostics_completion, + } + } +} + impl std::fmt::Debug for LanguageServerState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LanguageServerState::Starting(_) => { + LanguageServerState::Starting { .. } => { f.debug_struct("LanguageServerState::Starting").finish() } LanguageServerState::Running { .. } => { @@ -8455,20 +8501,27 @@ impl LspAdapter for SshLspAdapter { } } -pub fn language_server_settings<'a, 'b: 'a>( +pub fn language_server_settings<'a>( delegate: &'a dyn LspAdapterDelegate, language: &LanguageServerName, - cx: &'b AppContext, + cx: &'a AppContext, ) -> Option<&'a LspSettings> { - ProjectSettings::get( - Some(SettingsLocation { + language_server_settings_for( + SettingsLocation { worktree_id: delegate.worktree_id(), path: delegate.worktree_root_path(), - }), + }, + language, cx, ) - .lsp - .get(language) +} + +pub(crate) fn language_server_settings_for<'a>( + location: SettingsLocation<'a>, + language: &LanguageServerName, + cx: &'a AppContext, +) -> Option<&'a LspSettings> { + ProjectSettings::get(Some(location), cx).lsp.get(language) } pub struct LocalLspAdapterDelegate { @@ -8488,10 +8541,13 @@ impl LocalLspAdapterDelegate { worktree: &Model, http_client: Arc, fs: Arc, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Arc { - let worktree_id = worktree.read(cx).id(); - let worktree_abs_path = worktree.read(cx).abs_path(); + let (worktree_id, worktree_abs_path) = { + let worktree = worktree.read(cx); + (worktree.id(), worktree.abs_path()) + }; + let load_shell_env_task = environment.update(cx, |env, cx| { env.get_environment(Some(worktree_id), Some(worktree_abs_path), cx) }); @@ -8505,6 +8561,22 @@ impl LocalLspAdapterDelegate { load_shell_env_task, }) } + + fn from_local_lsp( + local: &LocalLspStore, + worktree: &Model, + cx: &mut AppContext, + ) -> Arc { + Self::new( + local.languages.clone(), + &local.environment, + local.weak.clone(), + worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + } } #[async_trait] @@ -8709,6 +8781,7 @@ async fn populate_labels_for_symbols( output.push(Symbol { language_server_name: symbol.language_server_name, source_worktree_id: symbol.source_worktree_id, + source_language_server_id: symbol.source_language_server_id, path: symbol.path, label: label.unwrap_or_else(|| CodeLabel::plain(name.clone(), None)), name, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index e707f9e9bc..36da95422d 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -40,7 +40,7 @@ pub struct PrettierStore { prettier_instances: HashMap, } -pub enum PrettierStoreEvent { +pub(crate) enum PrettierStoreEvent { LanguageServerRemoved(LanguageServerId), LanguageServerAdded { new_server_id: LanguageServerId, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 78965f64b5..d4f78757d8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task>> { 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, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task, 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, &'a Arc)> { - self.lsp_store - .read(cx) - .language_servers_for_local_buffer(buffer, cx) + pub fn language_server_for_id( + &self, + id: LanguageServerId, + cx: &AppContext, + ) -> Option> { + self.lsp_store.read(cx).language_server_for_id(id) + } + + pub fn for_language_servers_for_local_buffer( + &self, + buffer: &Buffer, + callback: impl FnOnce( + Box, &Arc)> + '_>, + ) -> 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 { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fe0bf8e0fa..1ed99b50cf 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -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::(|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>( diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs new file mode 100644 index 0000000000..2a172711f4 --- /dev/null +++ b/crates/project/src/project_tree.rs @@ -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, + worktree_store: Model, + _worktree_subscription: Subscription, +} + +impl WorktreeRoots { + fn new( + worktree_store: Model, + worktree: Model, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|cx| Self { + roots: 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>, + worktree_store: Model, + _subscriptions: [Subscription; 2], +} + +#[derive(Debug, Clone)] +struct AdapterWrapper(Arc); +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(&self, state: &mut H) { + self.0.name.hash(state); + } +} + +impl PartialOrd for AdapterWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + 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 for AdapterWrapper { + fn borrow(&self) -> &LanguageServerName { + &self.0.name + } +} + +#[derive(PartialEq)] +pub(crate) enum ProjectTreeEvent { + WorktreeRemoved(WorktreeId), + Cleared, +} + +impl EventEmitter for ProjectTree {} + +impl ProjectTree { + pub(crate) fn new(worktree_store: Model, cx: &mut AppContext) -> Model { + cx.new_model(|cx| Self { + root_points: Default::default(), + _subscriptions: [ + cx.subscribe(&worktree_store, Self::on_worktree_store_event), + cx.observe_global::(|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>, + delegate: Arc, + cx: &mut AppContext, + ) -> BTreeMap { + 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, + evt: &WorktreeStoreEvent, + cx: &mut ModelContext, + ) { + match evt { + WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { + self.root_points.remove(&worktree_id); + cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id)); + } + _ => {} + } + } +} diff --git a/crates/project/src/project_tree/path_trie.rs b/crates/project/src/project_tree/path_trie.rs new file mode 100644 index 0000000000..b2e2d82249 --- /dev/null +++ b/crates/project/src/project_tree/path_trie.rs @@ -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