From 0408a8259e89ce97ee43d289cecf3eeece89ade9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:05:00 +0200 Subject: [PATCH 1/6] Add `ModelContext::observe_global` --- crates/gpui/src/app.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5def0fcf5073a0424674143ec8ce930e31fab848..3c04ea16966f597dee8fd7b00ab2e18f94edbd21 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3120,6 +3120,19 @@ impl<'a, T: Entity> ModelContext<'a, T> { }) } + pub fn observe_global(&mut self, mut callback: F) -> Subscription + where + G: Any, + F: 'static + FnMut(&mut T, &mut ModelContext), + { + let observer = self.weak_handle(); + self.app.observe_global::(move |cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| callback(observer, cx)); + } + }) + } + pub fn observe_release( &mut self, handle: &ModelHandle, From 9d7476afc6f80a154be4d982ff21f7c3a82d51cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:05:35 +0200 Subject: [PATCH 2/6] Extract a `Project::stop_language_server` method when restarting server This will be useful later to stop a language server when detecting configuration changes. --- crates/project/src/project.rs | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 63a77f2642858d58e05a4dc7bfcefb1591c29a02..e49f3b424c4514df090d1b7f9a6276c1d31e0d09 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2060,6 +2060,35 @@ impl Project { }); } + fn stop_language_server( + &mut self, + worktree_id: WorktreeId, + adapter_name: LanguageServerName, + cx: &mut ModelContext, + ) -> Task<()> { + let key = (worktree_id, adapter_name); + self.language_servers.remove(&key); + if let Some(language_server) = self.started_language_servers.remove(&key) { + cx.spawn_weak(|this, mut cx| async move { + if let Some(language_server) = language_server.await { + if let Some(shutdown) = language_server.shutdown() { + shutdown.await; + } + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.language_server_statuses + .remove(&language_server.server_id()); + cx.notify(); + }); + } + } + }) + } else { + Task::ready(()) + } + } + pub fn restart_language_servers_for_buffers( &mut self, buffers: impl IntoIterator>, @@ -2096,20 +2125,11 @@ impl Project { } else { return; }; - let key = (worktree_id, adapter.name()); - let server_to_shutdown = self.language_servers.remove(&key); - self.started_language_servers.remove(&key); - server_to_shutdown - .as_ref() - .map(|(_, server)| self.language_server_statuses.remove(&server.server_id())); + + let stop = self.stop_language_server(worktree_id, adapter.name(), cx); cx.spawn_weak(|this, mut cx| async move { + stop.await; if let Some(this) = this.upgrade(&cx) { - if let Some((_, server_to_shutdown)) = server_to_shutdown { - if let Some(shutdown_task) = server_to_shutdown.shutdown() { - shutdown_task.await; - } - } - this.update(&mut cx, |this, cx| { this.start_language_server(worktree_id, worktree_path, language, cx); }); From 55cc8631cc5f0cf1f228d80825a21d12da213786 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:07:31 +0200 Subject: [PATCH 3/6] Introduce a new language-overrideable `enable_language_server` setting --- crates/settings/src/settings.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 0efedbfd9b8ae92ce4a91c5b3fb3cf59991a2580..c8c896b9d8024cfecd8349a8069f24702415dc8a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -28,6 +28,7 @@ pub struct Settings { pub soft_wrap: SoftWrap, pub preferred_line_length: u32, pub format_on_save: bool, + pub enable_language_server: bool, pub language_overrides: HashMap, LanguageOverride>, pub theme: Arc, } @@ -38,6 +39,7 @@ pub struct LanguageOverride { pub soft_wrap: Option, pub preferred_line_length: Option, pub format_on_save: Option, + pub enable_language_server: Option, } #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] @@ -60,6 +62,8 @@ pub struct SettingsFileContent { pub vim_mode: Option, #[serde(default)] pub format_on_save: Option, + #[serde(default)] + pub enable_language_server: Option, #[serde(flatten)] pub editor: LanguageOverride, #[serde(default)] @@ -84,6 +88,7 @@ impl Settings { preferred_line_length: 80, language_overrides: Default::default(), format_on_save: true, + enable_language_server: true, projects_online_by_default: true, theme, }) @@ -127,6 +132,13 @@ impl Settings { .unwrap_or(self.format_on_save) } + pub fn enable_language_server(&self, language: Option<&str>) -> bool { + language + .and_then(|language| self.language_overrides.get(language)) + .and_then(|settings| settings.enable_language_server) + .unwrap_or(self.enable_language_server) + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { @@ -138,6 +150,7 @@ impl Settings { soft_wrap: SoftWrap::None, preferred_line_length: 80, format_on_save: true, + enable_language_server: true, language_overrides: Default::default(), projects_online_by_default: true, theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()), @@ -177,6 +190,10 @@ impl Settings { merge(&mut self.default_buffer_font_size, data.buffer_font_size); merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.format_on_save, data.format_on_save); + merge( + &mut self.enable_language_server, + data.enable_language_server, + ); merge(&mut self.soft_wrap, data.editor.soft_wrap); merge(&mut self.tab_size, data.editor.tab_size); merge( @@ -193,6 +210,10 @@ impl Settings { merge_option(&mut target.tab_size, settings.tab_size); merge_option(&mut target.soft_wrap, settings.soft_wrap); merge_option(&mut target.format_on_save, settings.format_on_save); + merge_option( + &mut target.enable_language_server, + settings.enable_language_server, + ); merge_option( &mut target.preferred_line_length, settings.preferred_line_length, From 36a1a7a81909d83b82ca77a7c7f645b64b1db8d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:08:11 +0200 Subject: [PATCH 4/6] Start/stop language servers when `enable_language_server` changes --- crates/language/src/language.rs | 4 ++ crates/project/src/project.rs | 69 ++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fbc8182d30f5c8f483e801093f5db7c70a52c783..1d54bd35cdadb23a3e1a3df6dda6c81888682d73 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -240,6 +240,10 @@ impl LanguageRegistry { .cloned() } + pub fn to_vec(&self) -> Vec> { + self.languages.read().iter().cloned().collect() + } + pub fn language_names(&self) -> Vec { self.languages .read() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e49f3b424c4514df090d1b7f9a6276c1d31e0d09..c2859078cabbb6817ab795f6fc7d40b038698f57 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -90,7 +90,8 @@ pub struct Project { fs: Arc, client_state: ProjectClientState, collaborators: HashMap, - subscriptions: Vec, + client_subscriptions: Vec, + _subscriptions: Vec, opened_buffer: (Rc>>, watch::Receiver<()>), shared_buffers: HashMap>, loading_buffers: HashMap< @@ -418,7 +419,8 @@ impl Project { _maintain_remote_id_task, }, opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), - subscriptions: Vec::new(), + client_subscriptions: Vec::new(), + _subscriptions: vec![cx.observe_global::(Self::on_settings_changed)], active_entry: None, languages, client, @@ -503,7 +505,8 @@ impl Project { fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), - subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)], + client_subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)], + _subscriptions: Default::default(), client: client.clone(), client_state: ProjectClientState::Remote { sharing_has_stopped: false, @@ -650,6 +653,55 @@ impl Project { }) } + fn on_settings_changed(&mut self, cx: &mut ModelContext) { + let settings = cx.global::(); + + let mut language_servers_to_start = Vec::new(); + for buffer in self.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade(cx) { + let buffer = buffer.read(cx); + if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) + { + if settings.enable_language_server(Some(&language.name())) { + let worktree = file.worktree.read(cx); + language_servers_to_start.push(( + worktree.id(), + worktree.as_local().unwrap().abs_path().clone(), + language.clone(), + )); + } + } + } + } + + let mut language_servers_to_stop = Vec::new(); + for language in self.languages.to_vec() { + if let Some(lsp_adapter) = language.lsp_adapter() { + if !settings.enable_language_server(Some(&language.name())) { + let lsp_name = lsp_adapter.name(); + for (worktree_id, started_lsp_name) in self.started_language_servers.keys() { + if lsp_name == *started_lsp_name { + language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); + } + } + } + } + } + + // Stop all newly-disabled language servers. + for (worktree_id, adapter_name) in language_servers_to_stop { + self.stop_language_server(worktree_id, adapter_name, cx) + .detach(); + } + + // Start all the newly-enabled language servers. + for (worktree_id, worktree_path, language) in language_servers_to_start { + self.start_language_server(worktree_id, worktree_path, language, cx); + } + + cx.notify(); + } + pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { self.opened_buffers .get(&remote_id) @@ -775,7 +827,7 @@ impl Project { { *remote_id_tx.borrow_mut() = None; } - this.subscriptions.clear(); + this.client_subscriptions.clear(); this.metadata_changed(false, cx); }); response.map(drop) @@ -802,7 +854,7 @@ impl Project { this.metadata_changed(false, cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); - this.subscriptions + this.client_subscriptions .push(this.client.add_model_for_remote_entity(remote_id, cx)); Ok(()) }) @@ -1858,6 +1910,13 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { + if !cx + .global::() + .enable_language_server(Some(&language.name())) + { + return; + } + let adapter = if let Some(adapter) = language.lsp_adapter() { adapter } else { From 69170fc33a6c9f5f100e4eb65ff415bb273516a6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:48:06 +0200 Subject: [PATCH 5/6] Add unit test to ensure changing `enable_language_server` works --- crates/editor/src/editor.rs | 4 -- crates/project/Cargo.toml | 2 + crates/project/src/project.rs | 130 +++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8cd2e8a96df45138b46a2dcd095cea6020025566..b506b7b49b7c1e7f8866acd5520df636552a99df 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9089,7 +9089,6 @@ mod tests { #[gpui::test] async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -9202,7 +9201,6 @@ mod tests { #[gpui::test] async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -9316,8 +9314,6 @@ mod tests { #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); - let mut language = Language::new( LanguageConfig { name: "Rust".into(), diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 04597c0a3af1c6f4cedd3bdce92ba75eaf057475..eebfc08473758a3ff12d0498c1fc43d80d399852 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -11,6 +11,7 @@ doctest = false test-support = [ "client/test-support", "language/test-support", + "settings/test-support", "text/test-support", ] @@ -56,6 +57,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } tempdir = { version = "0.3.7" } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c2859078cabbb6817ab795f6fc7d40b038698f57..8c5d18f79f0c2fd81ad704dd7cb16d3085d2f3b4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -585,6 +585,10 @@ impl Project { root_paths: impl IntoIterator, cx: &mut gpui::TestAppContext, ) -> ModelHandle { + if !cx.read(|cx| cx.has_global::()) { + cx.update(|cx| cx.set_global(Settings::test(cx))); + } + let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); @@ -5751,7 +5755,7 @@ mod tests { use super::{Event, *}; use fs::RealFs; use futures::{future, StreamExt}; - use gpui::test::subscribe; + use gpui::{executor::Deterministic, test::subscribe}; use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, ToPoint, @@ -6503,6 +6507,130 @@ mod tests { }); } + #[gpui::test] + async fn test_toggling_enable_language_server( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + deterministic.forbid_parking(); + + let mut rust = Language::new( + LanguageConfig { + name: Arc::from("Rust"), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_rust_servers = rust.set_fake_lsp_adapter(FakeLspAdapter { + name: "rust-lsp", + ..Default::default() + }); + let mut js = Language::new( + LanguageConfig { + name: Arc::from("JavaScript"), + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_js_servers = js.set_fake_lsp_adapter(FakeLspAdapter { + name: "js-lsp", + ..Default::default() + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages.add(Arc::new(rust)); + project.languages.add(Arc::new(js)); + }); + + let _rs_buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .await + .unwrap(); + let _js_buffer = project + .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx)) + .await + .unwrap(); + + let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server_1 + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/a.rs" + ); + + let mut fake_js_server = fake_js_servers.next().await.unwrap(); + assert_eq!( + fake_js_server + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/b.js" + ); + + // Disable Rust language server, ensuring only that server gets stopped. + cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.language_overrides.insert( + Arc::from("Rust"), + settings::LanguageOverride { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }) + }); + fake_rust_server_1 + .receive_notification::() + .await; + + // Enable Rust and disable JavaScript language servers, ensuring that the + // former gets started again and that the latter stops. + cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.language_overrides.insert( + Arc::from("Rust"), + settings::LanguageOverride { + enable_language_server: Some(true), + ..Default::default() + }, + ); + settings.language_overrides.insert( + Arc::from("JavaScript"), + settings::LanguageOverride { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }) + }); + let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server_2 + .receive_notification::() + .await + .text_document + .uri + .as_str(), + "file:///dir/a.rs" + ); + fake_js_server + .receive_notification::() + .await; + } + #[gpui::test] async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); From 213b31607c02f2f18fb9f080a47f6c1698809f6a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jun 2022 10:59:02 +0200 Subject: [PATCH 6/6] Remove language server statuses synchronously when stopping a server --- crates/project/src/project.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8c5d18f79f0c2fd81ad704dd7cb16d3085d2f3b4..243ac5dcd5fed00b90b6059a4239bcb43928683e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2130,10 +2130,15 @@ impl Project { cx: &mut ModelContext, ) -> Task<()> { let key = (worktree_id, adapter_name); - self.language_servers.remove(&key); - if let Some(language_server) = self.started_language_servers.remove(&key) { + if let Some((_, language_server)) = self.language_servers.remove(&key) { + self.language_server_statuses + .remove(&language_server.server_id()); + cx.notify(); + } + + if let Some(started_language_server) = self.started_language_servers.remove(&key) { cx.spawn_weak(|this, mut cx| async move { - if let Some(language_server) = language_server.await { + if let Some(language_server) = started_language_server.await { if let Some(shutdown) = language_server.shutdown() { shutdown.await; }