diff --git a/Cargo.toml b/Cargo.toml index 555190ae7fd8ee..573732a73ebe36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,6 +442,7 @@ 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" diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index be9890dcb84275..6307e94cd19ee0 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4435,15 +4435,14 @@ async fn test_formatting_buffer( .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; - let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store()); let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); - let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer_b, cx) + let _handle = project_b.update(cx_b, |project, cx| { + project.register_buffer_with_language_servers(&buffer_b, cx) }); let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { diff --git a/crates/component/Cargo.toml b/crates/component/Cargo.toml index 33f951ff9520b2..3eef8a8d647a94 100644 --- a/crates/component/Cargo.toml +++ b/crates/component/Cargo.toml @@ -15,7 +15,7 @@ path = "src/component.rs" collections.workspace = true gpui.workspace = true linkme.workspace = true -once_cell = "1.20.3" +once_cell.workspace = true parking_lot.workspace = true theme.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 497f36f6034e11..362e7d3821480a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -458,12 +458,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 54e9533d7ea316..53f0921dffc54a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -130,7 +130,7 @@ use project::{ lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, - LspStore, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind, + PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind, }; use rand::prelude::*; use rpc::{proto::*, ErrorExt}; @@ -1486,9 +1486,8 @@ impl Editor { if let Some(buffer) = buffer.read(cx).as_singleton() { if let Some(project) = this.project.as_ref() { - let lsp_store = project.read(cx).lsp_store(); - let handle = lsp_store.update(cx, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer, cx) + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) }); this.registered_buffers .insert(buffer.read(cx).remote_id(), handle); @@ -1858,16 +1857,14 @@ impl Editor { fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { let buffers = self.buffer.read(cx).all_buffers(); - let Some(lsp_store) = self.lsp_store(cx) else { + let Some(project) = self.project.as_ref() else { return; }; - lsp_store.update(cx, |lsp_store, cx| { + project.update(cx, |project, cx| { for buffer in buffers { self.registered_buffers .entry(buffer.read(cx).remote_id()) - .or_insert_with(|| { - lsp_store.register_buffer_with_language_servers(&buffer, cx) - }); + .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); } }) } @@ -2067,14 +2064,14 @@ impl Editor { }; if let Some(buffer_id) = new_cursor_position.buffer_id { if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(lsp_store) = self.lsp_store(cx) { - lsp_store.update(cx, |lsp_store, cx| { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { return; }; self.registered_buffers.insert( buffer_id, - lsp_store.register_buffer_with_language_servers(&buffer, cx), + project.register_buffer_with_language_servers(&buffer, cx), ); }) } @@ -11519,7 +11516,10 @@ impl Editor { if let Some(project) = self.project.clone() { self.buffer.update(cx, |multi_buffer, cx| { project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(multi_buffer.all_buffers(), cx); + project.restart_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + cx, + ); }); }) } @@ -13815,12 +13815,6 @@ impl Editor { cx.notify(); } - pub fn lsp_store(&self, cx: &App) -> Option> { - self.project - .as_ref() - .map(|project| project.read(cx).lsp_store()) - } - fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { cx.notify(); } @@ -13847,11 +13841,11 @@ impl Editor { if let Some(buffer) = buffer_edited { let buffer_id = buffer.read(cx).remote_id(); if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(lsp_store) = self.lsp_store(cx) { - lsp_store.update(cx, |lsp_store, cx| { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { self.registered_buffers.insert( buffer_id, - lsp_store.register_buffer_with_language_servers(&buffer, cx), + project.register_buffer_with_language_servers(&buffer, cx), ); }) } @@ -13861,28 +13855,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), @@ -14474,15 +14467,18 @@ impl Editor { self.handle_input(text, window, cx); } - pub fn supports_inlay_hints(&self, cx: &App) -> bool { + pub fn supports_inlay_hints(&self, cx: &mut App) -> 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 } @@ -15026,7 +15022,7 @@ pub trait SemanticsProvider { cx: &mut App, ) -> Option>>; - fn supports_inlay_hints(&self, buffer: &Entity, cx: &App) -> bool; + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; fn document_highlights( &self, @@ -15420,17 +15416,25 @@ impl SemanticsProvider for Entity { })) } - fn supports_inlay_hints(&self, buffer: &Entity, cx: &App) -> bool { + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> 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 8b89a9f0faa60c..1c71a31751ecaa 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11320,7 +11320,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(), @@ -11333,13 +11332,13 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 1, "Should restart LSP server on a related LSP settings change" ); - update_test_project_settings(cx, |project_settings| { project_settings.lsp.insert( language_server_name.into(), @@ -11358,7 +11357,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test 1, "Should not restart LSP server on a related LSP settings change that is the same" ); - 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 e3b6cdb7247763..cbf9992f1d7394 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -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 1627d471c0f80c..a5dd583d6de918 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -467,7 +467,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { self.0.resolve_inlay_hint(hint, buffer, server_id, cx) } - fn supports_inlay_hints(&self, buffer: &Entity, cx: &App) -> bool { + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { if let Some(buffer) = self.to_base(&buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 137e3f80d92180..c0ff0029b56ec8 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -731,8 +731,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { // Start a new instance of the language server. project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer.clone()], cx) + project.restart_language_servers_for_buffers(vec![buffer.clone()], cx) }); + cx.executor().run_until_parked(); // The extension has cached the binary path, and does not attempt // to reinstall it. @@ -752,7 +753,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { cx.executor().run_until_parked(); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer.clone()], cx) + project.restart_language_servers_for_buffers(vec![buffer.clone()], cx) }); // The extension re-fetches the latest version of the language server. diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 228a0799ee719e..f14106d62af035 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -23,7 +23,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "banana": "", @@ -33,7 +33,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (picker, workspace, cx) = build_find_picker(project, cx); @@ -153,7 +153,7 @@ async fn test_complex_path(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "其他": { "S数据表格": { @@ -164,7 +164,7 @@ async fn test_complex_path(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (picker, workspace, cx) = build_find_picker(project, cx); @@ -194,7 +194,7 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { first_file_name: first_file_contents, @@ -204,7 +204,7 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (picker, workspace, cx) = build_find_picker(project, cx); @@ -269,7 +269,7 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { first_file_name: first_file_contents, @@ -279,7 +279,7 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (picker, workspace, cx) = build_find_picker(project, cx); @@ -1777,7 +1777,7 @@ async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "1.txt": "// One", "2.txt": "// Two", @@ -1785,7 +1785,7 @@ async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 8d64682b8da7a1..7b9ee6780e83e0 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -298,6 +298,7 @@ mod tests { use project::{FakeFs, Project}; use serde_json::json; use std::{num::NonZeroU32, sync::Arc, time::Duration}; + use util::path; use workspace::{AppState, Workspace}; #[gpui::test] @@ -305,7 +306,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": indoc!{" struct SingleLine; // display line 0 @@ -326,7 +327,7 @@ mod tests { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let worktree_id = workspace.update(cx, |workspace, cx| { @@ -335,7 +336,9 @@ mod tests { }) }); let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let editor = workspace @@ -414,14 +417,14 @@ mod tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": "ēlo" }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); workspace.update_in(cx, |workspace, window, cx| { @@ -437,7 +440,9 @@ mod tests { }) }); let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let editor = workspace @@ -497,14 +502,14 @@ mod tests { let text = "ēlo你好"; let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": text }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); workspace.update_in(cx, |workspace, window, cx| { @@ -520,7 +525,9 @@ mod tests { }) }); let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let editor = workspace @@ -573,14 +580,14 @@ mod tests { let text = "ēlo你好"; let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": text }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); workspace.update_in(cx, |workspace, window, cx| { @@ -596,7 +603,9 @@ mod tests { }) }); let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let editor = workspace diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 374698ff26ea76..02facaed4af672 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,7 +44,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, @@ -60,6 +59,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}; @@ -162,6 +162,7 @@ pub struct CachedLspAdapter { pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, + attach_kind: OnceLock, } impl Debug for CachedLspAdapter { @@ -197,6 +198,7 @@ impl CachedLspAdapter { adapter, cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), + attach_kind: Default::default(), }) } @@ -258,6 +260,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 @@ -268,6 +302,7 @@ pub trait LspAdapterDelegate: Send + Sync { fn http_client(&self) -> Arc; fn worktree_id(&self) -> WorktreeId; fn worktree_root_path(&self) -> &Path; + fn exists(&self, path: &Path, is_dir: Option) -> bool; fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus); async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; @@ -506,6 +541,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 21c083696f65c5..17555d60917ee4 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -108,6 +108,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>>>>, @@ -234,6 +235,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(), @@ -356,12 +358,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 } @@ -401,12 +407,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) } @@ -419,12 +430,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) @@ -895,6 +910,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 5241eb210d5e56..dcb0c5b595e07b 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -735,7 +735,8 @@ impl LspLogView { * Binary: {BINARY:#?} -* Running in project: {PATH:?} +* Registered workspace folders: +{WORKSPACE_FOLDERS} * Capabilities: {CAPABILITIES} @@ -743,7 +744,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 61167620fca036..9f996a55a72e75 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -74,6 +74,23 @@ 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"); + if delegate.exists(&p, Some(false)) { + 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/src/lsp.rs b/crates/lsp/src/lsp.rs index edbe564b795bea..47786da0e6ed60 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt}; use gpui::{App, AsyncApp, BackgroundExecutor, SharedString, Task}; +use notification::DidChangeWorkspaceFolders; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; use schemars::{ @@ -23,10 +24,11 @@ use smol::{ }; use std::{ + collections::BTreeSet, ffi::{OsStr, OsString}, fmt, io::Write, - ops::DerefMut, + ops::{Deref, DerefMut}, path::PathBuf, pin::Pin, sync::{ @@ -96,9 +98,8 @@ 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>>, } /// Identifies a running language server. @@ -376,8 +377,6 @@ impl LanguageServer { Some(stderr), stderr_capture, Some(server), - root_path, - working_dir, code_action_kinds, binary, cx, @@ -403,8 +402,6 @@ impl LanguageServer { stderr: Option, stderr_capture: Arc>>, server: Option, - root_path: &Path, - working_dir: &Path, code_action_kinds: Option>, binary: LanguageServerBinary, cx: AsyncApp, @@ -488,9 +485,8 @@ 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(), } } @@ -615,12 +611,11 @@ impl LanguageServer { } pub fn default_initialize_params(&self, cx: &App) -> 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 { @@ -790,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(), @@ -812,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: &App, ) -> 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 { @@ -1073,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) @@ -1210,6 +1190,118 @@ 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, + uri: Url, + language_id: String, + version: i32, + initial_text: String, + ) { + self.notify::(&DidOpenTextDocumentParams { + text_document: TextDocumentItem::new(uri, language_id, version, initial_text), + }) + .log_err(); + } + + pub fn unregister_buffer(&self, uri: Url) { + self.notify::(&DidCloseTextDocumentParams { + text_document: TextDocumentIdentifier::new(uri), + }) + .log_err(); + } } impl Drop for LanguageServer { @@ -1291,8 +1383,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( @@ -1303,8 +1393,6 @@ impl FakeLanguageServer { None::, Arc::new(Mutex::new(None)), None, - root, - root, None, binary.clone(), cx.clone(), @@ -1322,8 +1410,6 @@ impl FakeLanguageServer { None::, Arc::new(Mutex::new(None)), None, - root, - root, None, binary, cx.clone(), @@ -1360,16 +1446,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"))] @@ -1557,12 +1633,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/outline/src/outline.rs b/crates/outline/src/outline.rs index d44cc947966330..9e40d70e0e53e0 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -370,6 +370,7 @@ mod tests { use language::{Language, LanguageConfig, LanguageMatcher}; use project::{FakeFs, Project}; use serde_json::json; + use util::path; use workspace::{AppState, Workspace}; #[gpui::test] @@ -377,7 +378,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": indoc!{" struct SingleLine; // display line 0 @@ -391,7 +392,7 @@ mod tests { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); let (workspace, cx) = @@ -402,7 +403,9 @@ mod tests { }) }); let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let editor = workspace diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index b5b637ce04f88f..1c4e51240538ec 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5158,6 +5158,7 @@ mod tests { use project::FakeFs; use search::project_search::{self, perform_project_search}; use serde_json::json; + use util::path; use workspace::OpenVisible; use super::*; @@ -5535,8 +5536,8 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, "/rust-analyzer").await; - let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await; + populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await; + let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5580,15 +5581,17 @@ mod tests { ); }); }); - let all_matches = r#"/rust-analyzer/ + let root_path = format!("{}/", path!("/rust-analyzer")); + let all_matches = format!( + r#"{root_path} crates/ ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.param_names_for_lifetime_elision_hints {{ + search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{ + search: Some(it) if config.param_names_for_lifetime_elision_hints => {{ + search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }}, inlay_hints.rs search: pub param_names_for_lifetime_elision_hints: bool, search: param_names_for_lifetime_elision_hints: self @@ -5599,7 +5602,8 @@ mod tests { analysis_stats.rs search: param_names_for_lifetime_elision_hints: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#; + search: param_names_for_lifetime_elision_hints: self"# + ); let select_first_in_all_matches = |line_to_select: &str| { assert!(all_matches.contains(line_to_select)); all_matches.replacen( @@ -5910,7 +5914,7 @@ mod tests { async fn test_navigating_in_singleton(cx: &mut TestAppContext) { init_test(cx); - let root = "/root"; + let root = path!("/root"); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( root, @@ -5957,7 +5961,7 @@ struct OutlineEntryExcerpt { let _editor = workspace .update(cx, |workspace, window, cx| { - workspace.open_abs_path(PathBuf::from("/root/src/lib.rs"), true, window, cx) + workspace.open_abs_path(PathBuf::from(path!("/root/src/lib.rs")), true, window, cx) }) .unwrap() .await diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index a9254ac157d56b..40026b0c4e5ee8 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/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 68f1522af52ea8..6cd22fc852026f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -941,9 +941,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 b831b0e6810e70..5ef6338ad091e7 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::{AdapterQuery, LanguageServerTree, LaunchDisposition, ProjectTree}, relativize_path, resolve_path, toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -37,9 +38,9 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, 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, @@ -47,8 +48,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; @@ -80,6 +81,7 @@ use std::{ time::{Duration, Instant}, }; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, TransactionId}; +use url::Url; use util::{ debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt, TryFutureExt as _, @@ -132,13 +134,14 @@ impl FormatTrigger { } pub struct LocalLspStore { + weak: WeakEntity, worktree_store: Entity, toolchain_store: Entity, http_client: Arc, environment: Entity, fs: Arc, languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, yarn: Entity, pub language_servers: HashMap, buffers_being_formatted: HashSet, @@ -151,7 +154,6 @@ pub struct LocalLspStore { supplementary_language_servers: HashMap)>, prettier_store: Entity, - current_lsp_settings: HashMap, next_diagnostic_group_id: usize, diagnostics: HashMap< WorktreeId, @@ -165,6 +167,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, + lsp_tree: Entity, registered_buffers: HashMap, } @@ -178,7 +181,7 @@ impl LocalLspStore { match language_server_state { LanguageServerState::Running { server, .. } => Some(server), - LanguageServerState::Starting(_) => None, + LanguageServerState::Starting { .. } => None, } } @@ -187,26 +190,15 @@ impl LocalLspStore { worktree_handle: &Entity, delegate: Arc, adapter: Arc, - cx: &mut Context, - ) { + settings: Arc, + cx: &mut App, + ) -> 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()))); @@ -222,12 +214,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, @@ -254,13 +247,16 @@ 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(); + let this = self.weak.clone(); + let pending_workspace_folders = pending_workspace_folders.clone(); let fs = self.fs.clone(); - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |mut cx| async move { let result = { let delegate = delegate.clone(); let adapter = adapter.clone(); @@ -318,7 +314,7 @@ impl LocalLspStore { let language_server = cx .update(|cx| { language_server.initialize( - Some(initialization_params), + initialization_params, did_change_configuration_params.clone(), cx, ) @@ -352,6 +348,7 @@ impl LocalLspStore { server.clone(), server_id, key, + pending_workspace_folders, &mut cx, ); }) @@ -374,82 +371,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: &Entity, - language: LanguageName, - cx: &mut Context, - ) { - 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_entity(), - &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( @@ -457,7 +390,7 @@ impl LocalLspStore { adapter: Arc, delegate: Arc, allow_binary_download: bool, - cx: &mut Context, + cx: &mut App, ) -> Task> { let settings = ProjectSettings::get( Some(SettingsLocation { @@ -472,7 +405,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), @@ -493,7 +426,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( @@ -596,14 +529,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(), + )) } } }) @@ -1024,7 +959,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::>(); @@ -1040,42 +975,74 @@ 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_project_path( + &self, + project_path: ProjectPath, + language: &Language, + cx: &mut App, + ) -> Vec { + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(project_path.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( + project_path, + AdapterQuery::Language(&language.name()), + delegate, + cx, + ) + .filter_map(|node| node.server_id()) + .collect::>() + }); + + root + } + + fn language_server_ids_for_buffer( &self, buffer: &Buffer, - cx: &App, + cx: &mut App, ) -> 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 path: Arc = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + let worktree_path = ProjectPath { worktree_id, path }; + self.language_server_ids_for_project_path(worktree_path, language, cx) } else { Vec::new() } } - pub(crate) fn language_servers_for_buffer<'a>( + fn language_servers_for_buffer<'a>( &'a self, buffer: &'a Buffer, - cx: &'a App, + cx: &'a mut App, ) -> impl Iterator, &'a Arc)> { self.language_server_ids_for_buffer(buffer, cx) .into_iter() @@ -1090,7 +1057,7 @@ impl LocalLspStore { fn primary_language_server_for_buffer<'a>( &'a self, buffer: &'a Buffer, - cx: &'a App, + cx: &'a mut App, ) -> 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 @@ -1134,14 +1101,14 @@ impl LocalLspStore { let mut project_transaction = ProjectTransaction::default(); for buffer in &buffers { let adapters_and_servers = lsp_store.update(&mut cx, |lsp_store, cx| { - let buffer = buffer.handle.read(cx); - - lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>() + 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 settings = buffer.handle.update(&mut cx, |buffer, cx| { @@ -1252,11 +1219,13 @@ impl LocalLspStore { let operation = match formatter { Formatter::LanguageServer { name } => { let Some(language_server) = lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .as_local() - .unwrap() - .primary_language_server_for_buffer(buffer.handle.read(cx), cx) - .map(|(_, lsp)| lsp.clone()) + buffer.handle.update(cx, |buffer, cx| { + lsp_store + .as_local() + .unwrap() + .primary_language_server_for_buffer(buffer, cx) + .map(|(_, lsp)| lsp.clone()) + }) })? else { continue; @@ -1630,7 +1599,8 @@ impl LocalLspStore { fn initialize_buffer(&mut self, buffer_handle: &Entity, cx: &mut Context) { 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() { @@ -1648,60 +1618,64 @@ impl LocalLspStore { .log_err(); } } - let Some(language) = language else { return; }; 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 - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; + .get(&(worktree_id, adapter.name.clone())); + if let Some(server_ids) = servers { + for server_id in server_ids { + let server = self + .language_servers + .get(server_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 + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); + .and_then(|provider| { + provider + .trigger_characters + .as_ref() + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); + } + } } } pub(crate) fn reset_buffer(&mut self, buffer: &Entity, old_file: &File, cx: &mut App) { 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(language) = buffer.language() else { + return; + }; + let path = ProjectPath { + worktree_id: old_file.worktree_id(cx), + path: old_file.path.clone(), + }; + for server_id in self.language_server_ids_for_project_path(path, language, cx) { + buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); + buffer.set_completion_triggers(server_id, Default::default(), cx); } }); } @@ -1801,86 +1775,202 @@ 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 path: Arc = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + 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 }, + AdapterQuery::Language(&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(); - - let snapshot = LspBufferSnapshot { - version: 0, - snapshot: initial_snapshot.clone(), - }; - self.buffer_snapshots - .entry(buffer_id) - .or_default() - .insert(server.server_id(), vec![snapshot]); + for server in servers { + let snapshot = LspBufferSnapshot { + version: 0, + snapshot: initial_snapshot.clone(), + }; + self.buffer_snapshots + .entry(buffer_id) + .or_default() + .insert(server.server_id(), vec![snapshot]); + + server.register_buffer( + uri.clone(), + adapter.language_id(&language.name()), + 0, + initial_snapshot.text(), + ); + } } } + pub(crate) fn unregister_old_buffer_from_language_servers( &mut self, buffer: &Entity, old_file: &File, - cx: &mut App, ) { 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.as_path()).unwrap_or_else(|_| { - panic!( + + let Ok(file_url) = lsp::Url::from_file_path(old_path.as_path()) else { + debug_panic!( "`{}` is not parseable as an URI", old_path.to_string_lossy() - ) - }); - self.unregister_buffer_from_language_servers(buffer, file_url, cx); + ); + return; + }; + self.unregister_buffer_from_language_servers(buffer, &file_url, cx); } pub(crate) fn unregister_buffer_from_language_servers( &mut self, buffer: &Entity, - file_url: lsp::Url, + file_url: &lsp::Url, cx: &mut App, ) { buffer.update(cx, |buffer, cx| { - self.buffer_snapshots.remove(&buffer.remote_id()); + let _ = 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(file_url.clone()); } }); } @@ -2406,6 +2496,46 @@ impl LocalLspStore { failure_reason: None, }) } + + fn remove_worktree( + &mut self, + id_to_remove: WorktreeId, + cx: &mut Context<'_, 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, @@ -2742,6 +2872,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, @@ -2821,23 +2952,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: Entity, @@ -2869,8 +2983,10 @@ impl LspStore { sender, ) }; + let project_tree = ProjectTree::new(worktree_store.clone(), cx); Self { mode: LspStoreMode::Local(LocalLspStore { + weak: cx.weak_entity(), worktree_store: worktree_store.clone(), toolchain_store: toolchain_store.clone(), supplementary_language_servers: Default::default(), @@ -2881,7 +2997,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, @@ -2894,7 +3009,8 @@ 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), + registered_buffers: Default::default(), }), last_formatting_failure: None, downstream_client: None, @@ -2908,7 +3024,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), } } @@ -2969,17 +3085,6 @@ impl LspStore { } } - fn worktree_for_id( - &self, - worktree_id: WorktreeId, - cx: &Context, - ) -> 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, _: Entity, @@ -2992,9 +3097,10 @@ impl LspStore { } 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); } @@ -3128,15 +3234,13 @@ impl LspStore { Ok(()) } - pub fn register_buffer_with_language_servers( + pub(crate) fn register_buffer_with_language_servers( &mut self, buffer: &Entity, cx: &mut Context, ) -> OpenLspBufferHandle { let buffer_id = buffer.read(cx).remote_id(); - let handle = cx.new(|_| buffer.clone()); - if let Some(local) = self.as_local_mut() { let Some(file) = File::from_dyn(buffer.read(cx).file()) else { return handle; @@ -3144,12 +3248,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); } - 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 { @@ -3206,13 +3310,19 @@ 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, - ); + if let Some(file_url) = + lsp::Url::from_file_path(&f.abs_path(cx)) + .log_err() + { + local.unregister_buffer_from_language_servers( + &buffer, &file_url, cx, + ); + } } } } @@ -3289,24 +3399,29 @@ impl LspStore { pub(crate) fn set_language_for_buffer( &mut self, - buffer: &Entity, + buffer_entity: &Entity, new_language: Arc, cx: &mut Context, ) { - let buffer_file = buffer.read(cx).file().cloned(); - let buffer_id = buffer.read(cx).remote_id(); + let buffer = buffer_entity.read(cx); + let buffer_file = buffer.file().cloned(); + let buffer_id = buffer.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_entity, + &file_url, + cx, + ); } } } } - buffer.update(cx, |buffer, cx| { + buffer_entity.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { !Arc::ptr_eq(old_language, &new_language) }) { @@ -3323,7 +3438,7 @@ impl LspStore { 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_entity, cx); } } Some(worktree.read(cx).id()) @@ -3348,7 +3463,7 @@ impl LspStore { } cx.emit(LspStoreEvent::LanguageDetected { - buffer: buffer.clone(), + buffer: buffer_entity.clone(), new_language: Some(new_language), }) } @@ -3399,23 +3514,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, @@ -3424,6 +3539,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: {}", @@ -3435,6 +3551,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())); @@ -3509,25 +3626,13 @@ impl LspStore { } fn on_settings_changed(&mut self, cx: &mut Context) { - 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(), @@ -3535,61 +3640,7 @@ impl LspStore { } } - let mut language_servers_to_stop = Vec::new(); - let mut language_servers_to_restart = Vec::new(); - let languages = self.languages.to_vec(); - - let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone(); - let Some(current_lsp_settings) = self.swap_current_lsp_settings(new_lsp_settings.clone()) - else { - return; - }; - for (worktree_id, started_lsp_name) in - self.as_local().unwrap().language_server_ids.keys().cloned() - { - let language = languages.iter().find_map(|l| { - let adapter = self - .languages - .lsp_adapters(&l.name()) - .iter() - .find(|adapter| adapter.name == started_lsp_name)? - .clone(); - Some((l, adapter)) - }); - if let Some((language, adapter)) = language { - let worktree = self.worktree_for_id(worktree_id, cx).ok(); - let root_file = worktree.as_ref().and_then(|worktree| { - worktree - .update(cx, |tree, cx| tree.root_file(cx)) - .map(|f| f as _) - }); - let settings = language_settings(Some(language.name()), root_file.as_ref(), cx); - if !settings.enable_language_server { - language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); - } else if let Some(worktree) = worktree { - let server_name = &adapter.name; - match ( - current_lsp_settings.get(server_name), - new_lsp_settings.get(server_name), - ) { - (None, None) => {} - (Some(_), None) | (None, Some(_)) => { - language_servers_to_restart.push((worktree, language.name())); - } - (Some(current_lsp_settings), Some(new_lsp_settings)) => { - if current_lsp_settings != new_lsp_settings { - language_servers_to_restart.push((worktree, language.name())); - } - } - } - } - } - } - - for (worktree_id, adapter_name) in language_servers_to_stop { - self.stop_local_language_server(worktree_id, adapter_name, cx) - .detach(); - } + self.refresh_server_tree(cx); if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { @@ -3597,19 +3648,141 @@ impl LspStore { }) } - // 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); - } + cx.notify(); + } - // Restart all language servers with changed initialization options. - for (worktree, language) in language_servers_to_restart { - self.restart_local_language_servers(worktree, language, cx); - } + fn refresh_server_tree(&mut self, cx: &mut Context<'_, Self>) { + let buffer_store = self.buffer_store.clone(); + if let Some(local) = self.as_local_mut() { + let mut adapters = BTreeMap::default(); + let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { + let 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 App| { + 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, + )) + } + }; - cx.notify(); + let mut rebase = lsp_tree.rebase(); + for buffer in buffer_store.read(cx).buffers().collect::>() { + let buffer = buffer.read(cx); + if !local.registered_buffers.contains_key(&buffer.remote_id()) { + continue; + } + if let Some((file, language)) = File::from_dyn(buffer.file()) + .cloned() + .zip(buffer.language().map(|l| l.name())) + { + let worktree_id = file.worktree_id(cx); + let Some(worktree) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; + let path: Arc = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + let worktree_path = ProjectPath { worktree_id, path }; + + let Some(delegate) = adapters + .entry(worktree_id) + .or_insert_with(|| get_adapter(worktree_id, cx)) + .clone() + else { + continue; + }; + let nodes = rebase.get( + worktree_path, + AdapterQuery::Language(&language), + delegate.clone(), + cx, + ); + for node in nodes { + 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) = local + .language_server_ids + .get(&(worktree_id, server_name.clone())) + { + server_ids.iter().cloned().next().unwrap() + } else { + local.start_language_server( + &worktree, + delegate.clone(), + local + .languages + .lsp_adapters(&language) + .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()); + local.language_server_ids.remove(&key); + + let server_id = local.start_language_server( + &worktree, + delegate.clone(), + local + .languages + .lsp_adapters(&language) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"), + settings, + cx, + ); + if let Some(state) = local.language_servers.get(&server_id) + { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } + server_id + } + }, + ); + } + } + } + rebase.finish() + }); + for (id, name) in to_stop { + self.stop_local_language_server(id, name, cx).detach(); + } + } } pub fn apply_code_action( @@ -3640,12 +3813,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 { @@ -3726,19 +3897,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), @@ -3771,22 +3939,24 @@ impl LspStore { let Some(server_id) = self .as_local() .and_then(|local| { - local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| { - server - .capabilities() - .linked_editing_range_provider - .is_some() - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) - .next() + buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(_, server)| { + server + .capabilities() + .linked_editing_range_provider + .is_some() + }) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) + .next() + }) }) .or_else(|| { self.upstream_client() @@ -4040,17 +4210,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 { @@ -4371,10 +4543,9 @@ impl LspStore { push_to_history: bool, cx: &mut Context, ) -> 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(); @@ -4413,9 +4584,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(); @@ -4706,6 +4882,7 @@ impl LspStore { }) } else if let Some(local) = self.as_local() { struct WorkspaceSymbolsResult { + server_id: LanguageServerId, lsp_adapter: Arc, worktree: WeakEntity, worktree_abs_path: Arc, @@ -4713,7 +4890,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) @@ -4725,55 +4903,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 { @@ -4813,6 +4999,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, @@ -4892,19 +5079,20 @@ impl LspStore { buffer: Entity, cx: &mut Context, ) -> 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(); @@ -5021,7 +5209,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); } @@ -5042,31 +5233,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_entity(), - &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_entity(), + &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::>() }) @@ -5130,27 +5327,31 @@ impl LspStore { pub(crate) fn language_servers_for_local_buffer<'a>( &'a self, - buffer: &'a Buffer, - cx: &'a App, + buffer: &Buffer, + cx: &mut App, ) -> 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 App, + cx: &'a mut App, ) -> Option<(&'a Arc, &'a Arc)> { self.as_local()? .language_servers_for_buffer(buffer, cx) @@ -5159,40 +5360,12 @@ impl LspStore { fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut Context) { 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( @@ -5252,14 +5425,39 @@ impl LspStore { fn register_local_language_server( &mut self, - worktree_id: WorktreeId, + worktree: Entity, language_server_name: LanguageServerName, language_server_id: LanguageServerId, + cx: &mut App, ) { - self.as_local_mut() - .unwrap() + let Some(local) = self.as_local_mut() else { + return; + }; + let worktree_id = worktree.read(cx).id(); + let path = ProjectPath { + worktree_id, + path: Arc::from("".as_ref()), + }; + let delegate = LocalLspAdapterDelegate::from_local_lsp(local, &worktree, cx); + local.lsp_tree.update(cx, |this, cx| { + for node in this.get( + path, + AdapterQuery::Adapter(&language_server_name), + delegate, + cx, + ) { + node.server_id_or_init(|disposition| { + assert_eq!(disposition.server_name, &language_server_name); + + language_server_id + }); + } + }); + local .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( @@ -5395,10 +5593,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" ))); @@ -5491,9 +5696,10 @@ impl LspStore { lsp_store .update(&mut cx, |lsp_store, cx| { lsp_store.register_local_language_server( - worktree.read(cx).id(), + worktree.clone(), language_server_name, language_server_id, + cx, ) }) .ok(); @@ -5539,16 +5745,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() @@ -6290,18 +6499,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 } @@ -6698,6 +6902,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, @@ -7032,14 +7237,14 @@ impl LspStore { cx: AsyncApp, ) { 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", @@ -7066,37 +7271,33 @@ impl LspStore { // for the stopped server fn stop_local_language_server( &mut self, - worktree_id: WorktreeId, - adapter_name: LanguageServerName, + server_id: LanguageServerId, + name: LanguageServerName, cx: &mut Context, ) -> 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 { - 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 mut orphaned_worktrees = vec![]; + // 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; } - } + if ids.is_empty() { + orphaned_worktrees.push(*worktree); + false + } else { + true + } + }); + let _ = self.language_server_statuses.remove(&server_id); + log::info!("stopping language server {name}"); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { buffer.update(cx, |buffer, cx| { @@ -7130,7 +7331,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| { @@ -7154,7 +7354,7 @@ impl LspStore { pub fn restart_language_servers_for_buffers( &mut self, - buffers: impl IntoIterator>, + buffers: Vec>, cx: &mut Context, ) { if let Some((client, project_id)) = self.upstream_client() { @@ -7169,81 +7369,45 @@ impl LspStore { .spawn(request) .detach_and_log_err(cx); } else { - let language_server_lookup_info: HashSet<(Entity, LanguageName)> = buffers + let Some(local) = self.as_local_mut() else { + return; + }; + let language_servers_to_stop = buffers + .iter() + .flat_map(|buffer| { + buffer.update(cx, |buffer, cx| { + local.language_server_ids_for_buffer(buffer, cx) + }) + }) + .collect::>(); + local.lsp_tree.update(cx, |this, _| { + this.remove_nodes(&language_servers_to_stop); + }); + let tasks = language_servers_to_stop .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())) + .map(|server| { + let name = self + .language_server_statuses + .get(&server) + .map(|state| state.name.as_str().into()) + .unwrap_or_else(|| LanguageServerName::from("Unknown")); + self.stop_local_language_server(server, name, cx) }) - .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: Entity, - language: LanguageName, - cx: &mut Context, - ) { - let worktree_id = worktree.read(cx).id(); - - let stop_tasks = self - .languages - .clone() - .lsp_adapters(&language) - .iter() - .map(|adapter| { - let stop_task = - self.stop_local_language_server(worktree_id, adapter.name.clone(), cx); - (stop_task, adapter.name.clone()) - }) - .collect::>(); - if stop_tasks.is_empty() { - return; - } - - cx.spawn(move |this, mut cx| async move { - // For each stopped language server, record all of the worktrees with which - // it was associated. - let mut affected_worktrees = Vec::new(); - for (stop_task, language_server_name) in stop_tasks { - for affected_worktree_id in stop_task.await { - affected_worktrees.push((affected_worktree_id, language_server_name.clone())); - } - } - - 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); + .collect::>(); - // 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); + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .spawn(futures::future::join_all(tasks)) + .await; + this.update(&mut cx, |this, cx| { + for buffer in buffers { + this.register_buffer_with_language_servers(&buffer, cx); } - } + }) + .ok() }) - .ok(); - }) - .detach(); + .detach(); + } } pub fn update_diagnostics( @@ -7367,12 +7531,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 Context, ) { let Some(local) = self.as_local_mut() else { @@ -7383,7 +7549,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; @@ -7393,11 +7559,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() @@ -7483,6 +7650,12 @@ impl LspStore { .entry(buffer.remote_id()) .or_default() .entry(server_id) + .and_modify(|_| { + assert!( + false, + "There should not be an existing snapshot for a newly inserted buffer" + ) + }) .or_insert_with(|| { vec![LspBufferSnapshot { version: 0, @@ -7494,20 +7667,13 @@ impl LspStore { 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(); + language_server.register_buffer( + uri, + adapter.language_id(&language.name()), + version, + initial_snapshot.text(), + ); } - buffer_handle.update(cx, |buffer, cx| { buffer.set_completion_triggers( server_id, @@ -7569,12 +7735,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); } @@ -7607,16 +7772,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() { @@ -7663,7 +7818,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| { @@ -7706,8 +7861,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(); @@ -7768,6 +7925,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.as_ref().to_proto(), name: symbol.name.clone(), @@ -7802,6 +7960,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)) @@ -8195,7 +8356,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, @@ -8204,10 +8369,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 { .. } => { @@ -8362,20 +8567,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 App, + cx: &'a App, ) -> 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 App, +) -> Option<&'a LspSettings> { + ProjectSettings::get(Some(location), cx).lsp.get(language) } pub struct LocalLspAdapterDelegate { @@ -8395,10 +8607,13 @@ impl LocalLspAdapterDelegate { worktree: &Entity, http_client: Arc, fs: Arc, - cx: &mut Context, + cx: &mut App, ) -> 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) }); @@ -8412,6 +8627,22 @@ impl LocalLspAdapterDelegate { load_shell_env_task, }) } + + fn from_local_lsp( + local: &LocalLspStore, + worktree: &Entity, + cx: &mut App, + ) -> Arc { + Self::new( + local.languages.clone(), + &local.environment, + local.weak.clone(), + worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + } } #[async_trait] @@ -8432,6 +8663,14 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { self.worktree.id() } + fn exists(&self, path: &Path, is_dir: Option) -> bool { + self.worktree.entry_for_path(path).map_or(false, |entry| { + is_dir.map_or(true, |is_required_to_be_dir| { + is_required_to_be_dir == entry.is_dir() + }) + }) + } + fn worktree_root_path(&self) -> &Path { self.worktree.abs_path().as_ref() } @@ -8616,6 +8855,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 510c426b9d764d..fdb76f81b22188 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 5446471b90f4d5..8b3e28fd5cc6e9 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; @@ -66,7 +67,7 @@ use lsp::{ LanguageServerId, LanguageServerName, MessageActionItem, }; use lsp_command::*; -use lsp_store::LspFormatTarget; +use lsp_store::{LspFormatTarget, OpenLspBufferHandle}; use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; @@ -478,6 +479,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, @@ -1938,7 +1940,7 @@ impl Project { pub fn open_buffer( &mut self, path: impl Into, - cx: &mut Context, + cx: &mut App, ) -> Task>> { if self.is_disconnected(cx) { return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); @@ -1956,16 +1958,25 @@ impl Project { cx: &mut Context, ) -> 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(|this, 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) + let handle = this.update(&mut cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) })?; Ok((buffer, handle)) }) } + pub fn register_buffer_with_language_servers( + &self, + buffer: &Entity, + cx: &mut App, + ) -> OpenLspBufferHandle { + self.lsp_store.update(cx, |lsp_store, cx| { + lsp_store.register_buffer_with_language_servers(&buffer, cx) + }) + } + pub fn open_unstaged_diff( &mut self, buffer: Entity, @@ -2584,7 +2595,7 @@ impl Project { pub fn restart_language_servers_for_buffers( &mut self, - buffers: impl IntoIterator>, + buffers: Vec>, cx: &mut Context, ) { self.lsp_store.update(cx, |lsp_store, cx| { @@ -4311,14 +4322,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 App, - ) -> 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: &App, + ) -> 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 App, + ) -> R { + self.lsp_store.update(cx, |this, cx| { + callback(Box::new(this.language_servers_for_local_buffer(buffer, cx))) + }) } pub fn buffer_store(&self) -> &Entity { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a002431253c210..ac56e5748c6b20 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1406,14 +1406,13 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC }) .await .unwrap(); - // Simulate diagnostics starting to update. let fake_server = fake_servers.next().await.unwrap(); fake_server.start_progress(progress_token).await; - + drop(_handle); // Restart the server before the diagnostics finish updating. project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer], cx); + project.restart_language_servers_for_buffers(vec![buffer], cx); }); let mut events = cx.events(&project); @@ -1518,7 +1517,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer.clone()], cx); + project.restart_language_servers_for_buffers(vec![buffer.clone()], cx); }); // The diagnostics are cleared. @@ -1572,10 +1571,10 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T diagnostics: Vec::new(), }); cx.executor().run_until_parked(); - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer.clone()], cx); + project.restart_language_servers_for_buffers(vec![buffer.clone()], cx); }); + cx.run_until_parked(); let mut fake_server = fake_servers.next().await.unwrap(); let notification = fake_server .receive_notification::() @@ -1752,6 +1751,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(path!("/dir/a.rs"), cx) + }) + .await + .unwrap(); let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); assert_eq!( fake_rust_server_2 @@ -1782,7 +1787,6 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { fs.insert_tree(path!("/dir"), json!({ "a.rs": text })).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1801,8 +1805,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let _handle = lsp_store.update(cx, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer, cx) + let _handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) }); let mut fake_server = fake_servers.next().await.unwrap(); @@ -2582,25 +2586,28 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { fs.insert_tree( path!("/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, [path!("/dir/b.rs").as_ref()], cx).await; + let project = Project::test(fs, [path!("/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(path!("/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; @@ -2612,12 +2619,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + lsp::Url::from_file_path(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 @@ -2638,14 +2644,14 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { .as_local() .unwrap() .abs_path(cx), - Path::new(path!("/dir/a.rs")), + Path::new(path!("/another_dir/a.rs")), ); assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); assert_eq!( list_worktrees(&project, cx), [ - (path!("/dir/a.rs").as_ref(), false), - (path!("/dir/b.rs").as_ref(), true) + (path!("/another_dir/a.rs").as_ref(), false), + (path!("/dir").as_ref(), true) ], ); @@ -2654,7 +2660,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { cx.update(|cx| { assert_eq!( list_worktrees(&project, cx), - [(path!("/dir/b.rs").as_ref(), true)] + [(path!("/dir").as_ref(), true)] ); }); @@ -3381,7 +3387,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a": { "file1": "", @@ -3390,7 +3396,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, [Path::new("/dir")], cx).await; + let project = Project::test(fs, [Path::new(path!("/dir"))], cx).await; let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); let tree_id = tree.update(cx, |tree, _| tree.id()); diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs new file mode 100644 index 00000000000000..4f13a0c7ec8592 --- /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::{App, AppContext, Context, Entity, EventEmitter, Subscription}; +use language::{CachedLspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerName; +use path_trie::{LabelPresence, RootPathTrie, TriePath}; +use settings::{SettingsStore, WorktreeId}; +use worktree::{Event as WorktreeEvent, Worktree}; + +use crate::{ + worktree_store::{WorktreeStore, WorktreeStoreEvent}, + ProjectPath, +}; + +pub(crate) use server_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition}; + +struct WorktreeRoots { + roots: RootPathTrie, + worktree_store: Entity, + _worktree_subscription: Subscription, +} + +impl WorktreeRoots { + fn new( + worktree_store: Entity, + worktree: Entity, + cx: &mut App, + ) -> Entity { + cx.new(|cx| Self { + roots: RootPathTrie::new(), + worktree_store, + _worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| { + match event { + WorktreeEvent::UpdatedEntries(changes) => { + for (path, _, kind) in changes.iter() { + match kind { + worktree::PathChange::Removed => { + let path = TriePath::from(path.as_ref()); + this.roots.remove(&path); + } + _ => {} + } + } + } + WorktreeEvent::UpdatedGitRepositories(_) => {} + WorktreeEvent::DeletedEntry(entry_id) => { + let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx) + else { + return; + }; + let path = TriePath::from(entry.path.as_ref()); + this.roots.remove(&path); + } + } + }), + }) + } +} + +pub struct ProjectTree { + root_points: HashMap>, + worktree_store: Entity, + _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: Entity, cx: &mut App) -> Entity { + cx.new(|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 App, + ) -> 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, + _: Entity, + evt: &WorktreeStoreEvent, + cx: &mut Context, + ) { + 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 00000000000000..1c70efe7b55c0f --- /dev/null +++ b/crates/project/src/project_tree/path_trie.rs @@ -0,0 +1,240 @@ +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