From bed917b0b11660fc616dd3c04df050c0f4895773 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:31:14 +0100 Subject: [PATCH] project: Allow running multiple instances of a single language server within a single worktree (#22182) 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 `workspaceFolder`s 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 #5108 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 | 1595 +++++++++-------- 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, 2154 insertions(+), 946 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 2544a53fc0c14022c32ebd32e11401f2d552ba93..a7920d44783047e1a18dfaa859c7c081f9fbbed8 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 e7481808e22e99c7a74483b3803e7a4e3c7a8918..b958452a02ba9dcbd8acf7ee8acffe8a67d9e513 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 020a86c9f80522d0eefa8a3cc2ece0f4f4352fd2..7437546cba42435ac5bb55fd47972c5ffe0465c2 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 67280765f6643437808a19bb063744efa409edb6..9ffb5f3fb9691a8f63aea63c9eb509066652ad92 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 9efc74c787cfd60c28a03bd7bbe05cdb8d103dcf..ad13de90b65c0672e355cda8df9a4cb16fc263b1 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 6f789acce766e81c7cc12d912582c7ba75ab7e40..f0a9fd90af512952a09099e53ea48e8783adf1f1 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 2937e75943970cf3f7011b053daecff6ccd961ca..e4c28243a1a0309139f0648890a82a33318ac5ce 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 923dcc24b9f04771763d71bdaafcaedeb6579c05..abfda04e7b49af2e8a2894a1dad5ba362b9ff53a 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 693606619275ae8581a6d9e23e0b1258b992a2e4..e605bbb2f2e698bd8af2522127dc92298b02ce21 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 8061c7efa9879ed11dadea87c25fe80a22be1a0d..2769e0fe0bc86356852db31a8409bcc9257d267f 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 794ab0784ea3cdece02f8724df70b64cdb063581..51d464d409752a4915f01fd67876a4b0ce723499 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 184a06f21589ac95a03bb394c298e5da7ea46337..62cfdeedcbce7545d39b5228c4220291b15a2d40 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 3ef27476427e450a98940aa612fbb21881ee5c9f..cdd094a129c69e0f5b085413efc96eabbcd3981a 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 0937b47217c09fc74b54f3ab1ebd4ae5aaa61fa8..eba4d0e8707cf14135453af6db52c28a47e4c747 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 e9fa1caac239892e2f1a3aed1c923fbdcb9763aa..51de3bc9ce0fcf2046817dd6fb297a7a09114778 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 b9fcd8df0e24595de80e91891307863849153afd..c0d770c5d5760522e2f1ad13e3ef4f525b686310 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 5149a818cf55787e20f2f26af00f49bd0980187c..7c2c546e702e37168f2ac3979c3b1b4506273b3b 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 c58fcb1351e1fef7c1404934aa4bb635ceccb33d..e69b7b95b04207d9c599b1f6b779eac882507a4e 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 ba88b5e48503204468322720daa0f36ef0ce699d..9e3960c9254d17eb6990fbf04138eaeb67b1d157 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 = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); + 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 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 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, + )) + } + }; - 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)) + 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, + ); }); - 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())); - } - } - } - } - } } - - for (worktree_id, adapter_name) in language_servers_to_stop { - self.stop_local_language_server(worktree_id, adapter_name, cx) - .detach(); + 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), @@ -3871,24 +3856,26 @@ impl LspStore { let snapshot = buffer.read(cx).snapshot(); let scope = snapshot.language_scope_at(position); 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() + .as_local() + .and_then(|local| { + 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), - - _ => 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| { + 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 '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::>() + }).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 { + server_id, + lsp_adapter, + worktree: worktree_handle.downgrade(), + worktree_abs_path, + lsp_symbols, } - }).unwrap_or_default(); - - WorkspaceSymbolsResult { - 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,40 +5270,12 @@ 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); - } - } - 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)); + let to_remove = local.remove_worktree(id_to_remove, cx); + for server in to_remove { + self.language_server_statuses.remove(&server); } } - for server in to_remove { - self.language_server_statuses.remove(&server); - } } pub fn shared( @@ -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(); - - for (worktree, language) in language_server_lookup_info { - self.restart_local_language_servers(worktree, language, cx); - } - } - } - - 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; - } + .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 + }, + ); - 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())); + // 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); + + 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 e707f9e9bc2d20d906bd69f01efdb7586a0a59cd..36da95422def86a25e51b182fd04a29d67b5ad9a 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 78965f64b58aeb83cd76bc427d6fc1a881b81d07..d4f78757d80da808e2b97cdfcdd0cfea27dabb2f 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 fe0bf8e0fa6291d6b435237e4ab05d1d6eb53f94..1ed99b50cfce70cf6c0345dbd4448d41af592599 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 0000000000000000000000000000000000000000..2a172711f45b7b011d2571a878bbd4672fd7992c --- /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 0000000000000000000000000000000000000000..b2e2d8224906db32679276200fb1629c2534ebbd --- /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