From 84802ba81cee83a093959864c9669a2e7ccbe2e1 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 13 Mar 2026 11:11:14 +0100 Subject: [PATCH 1/2] Prevent auto-restart of language servers after Stop All Servers When the user clicks Stop All Servers, set a flag that suppresses automatic language server startup. The flag is cleared when Restart All Servers is hit. This guards the three auto-start paths: - Buffer registration with language servers (new buffers, file path changes, language detection, language registry reloads) - Server tree refresh (workspace config / manifest changes) - Settings-triggered restarts (semantic token mode changes) --- crates/project/src/lsp_store.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d679b531c83445..237b9afde74131 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -339,6 +339,7 @@ pub struct LocalLspStore { HashMap, HashMap>>, >, restricted_worktrees_tasks: HashMap)>, + language_servers_stopped: bool, buffers_to_refresh_hash_set: HashSet, buffers_to_refresh_queue: VecDeque, @@ -2829,6 +2830,9 @@ impl LocalLspStore { only_register_servers: HashSet, cx: &mut Context, ) { + if self.language_servers_stopped { + return; + } let buffer = buffer_handle.read(cx); let buffer_id = buffer.remote_id(); @@ -4373,6 +4377,7 @@ impl LspStore { buffer_pull_diagnostics_result_ids: HashMap::default(), workspace_pull_diagnostics_result_ids: HashMap::default(), restricted_worktrees_tasks: HashMap::default(), + language_servers_stopped: false, watched_manifest_filenames: ManifestProvidersStore::global(cx) .manifest_file_names(), }), @@ -5430,7 +5435,12 @@ impl LspStore { .semantic_token_config .update_global_mode(new_global_semantic_tokens_mode) { - self.restart_all_language_servers(cx); + let stopped = self + .as_local() + .is_some_and(|local| local.language_servers_stopped); + if !stopped { + self.restart_all_language_servers(cx); + } } cx.notify(); @@ -5441,6 +5451,9 @@ impl LspStore { let Some(local) = self.as_local_mut() else { return; }; + if local.language_servers_stopped { + return; + } let mut adapters = BTreeMap::default(); let get_adapter = { let languages = local.languages.clone(); @@ -11373,6 +11386,9 @@ impl LspStore { } pub fn stop_all_language_servers(&mut self, cx: &mut Context) { + if let Some(local) = self.as_local_mut() { + local.language_servers_stopped = true; + } self.shutdown_all_language_servers(cx).detach(); } @@ -11408,6 +11424,9 @@ impl LspStore { } pub fn restart_all_language_servers(&mut self, cx: &mut Context) { + if let Some(local) = self.as_local_mut() { + local.language_servers_stopped = false; + } let buffers = self.buffer_store.read(cx).buffers().collect(); self.restart_language_servers_for_buffers(buffers, HashSet::default(), cx); } From c83720e888935b644ccf2a12901c37e285d888ee Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 8 Apr 2026 11:56:38 +0100 Subject: [PATCH 2/2] Prevent auto-restart of language servers after stopping single servers Extend the stop-suppresses-restart mechanism to also work when stopping individual language servers, not just "Stop All Servers". Replace the single language_servers_stopped boolean with two fields: - all_language_servers_stopped: set by Stop All Servers - stopped_language_servers: tracks individually-stopped servers by name When a user stops a specific language server, its name is recorded in the stopped set. The three auto-start paths (buffer registration, server tree refresh, settings-triggered restarts) now skip servers in this set. Restarting a specific server clears it from the stopped set. Restart All Servers clears both the global flag and the per-server set. --- .../remote_editing_collaboration_tests.rs | 2 +- crates/editor/src/editor.rs | 1 + .../src/extension_store_test.rs | 14 ++- crates/language_tools/src/lsp_button.rs | 1 + crates/project/src/lsp_store.rs | 89 +++++++++++++++---- crates/project/src/project.rs | 8 +- .../tests/integration/project_tests.rs | 24 ++++- 7 files changed, 115 insertions(+), 24 deletions(-) diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index d82971fe7a6489..87784b6328149d 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -934,7 +934,7 @@ async fn test_ssh_restarting_language_server_replaces_remote_status( }); project_a.update(cx_a, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx); + project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), true, cx); }); let restarted_server = fake_language_servers.next().await.unwrap(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8eca77a5fa40ae..ad9f698ac6c9cf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8005,6 +8005,7 @@ impl Editor { project.restart_language_servers_for_buffers( multi_buffer.all_buffers().into_iter().collect(), HashSet::default(), + true, cx, ); }); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 011e1a1d6bc26f..6868701eb9e20e 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -952,7 +952,12 @@ 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(vec![buffer.clone()], HashSet::default(), cx) + project.restart_language_servers_for_buffers( + vec![buffer.clone()], + HashSet::default(), + true, + cx, + ) }); cx.executor().run_until_parked(); @@ -993,7 +998,12 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { .await; cx.executor().run_until_parked(); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) + project.restart_language_servers_for_buffers( + vec![buffer.clone()], + HashSet::default(), + true, + cx, + ) }); // The extension re-fetches the latest version of the language server. diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index e7c6d5b2160415..22b2795145fd20 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -551,6 +551,7 @@ impl LanguageServerState { HashSet::from_iter([LanguageServerSelector::Name( server_name_for_restart.clone(), )]), + true, cx, ); }) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 237b9afde74131..4cbda008b92e91 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -339,7 +339,8 @@ pub struct LocalLspStore { HashMap, HashMap>>, >, restricted_worktrees_tasks: HashMap)>, - language_servers_stopped: bool, + all_language_servers_stopped: bool, + stopped_language_servers: HashSet, buffers_to_refresh_hash_set: HashSet, buffers_to_refresh_queue: VecDeque, @@ -2830,7 +2831,7 @@ impl LocalLspStore { only_register_servers: HashSet, cx: &mut Context, ) { - if self.language_servers_stopped { + if self.all_language_servers_stopped { return; } let buffer = buffer_handle.read(cx); @@ -2892,6 +2893,11 @@ impl LocalLspStore { if reused && server_node.server_id().is_none() { return None; } + if let Some(name) = server_node.name() + && self.stopped_language_servers.contains(&name) + { + return None; + } if !only_register_servers.is_empty() { if let Some(server_id) = server_node.server_id() && !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) @@ -4377,7 +4383,8 @@ impl LspStore { buffer_pull_diagnostics_result_ids: HashMap::default(), workspace_pull_diagnostics_result_ids: HashMap::default(), restricted_worktrees_tasks: HashMap::default(), - language_servers_stopped: false, + all_language_servers_stopped: false, + stopped_language_servers: HashSet::default(), watched_manifest_filenames: ManifestProvidersStore::global(cx) .manifest_file_names(), }), @@ -5435,11 +5442,15 @@ impl LspStore { .semantic_token_config .update_global_mode(new_global_semantic_tokens_mode) { - let stopped = self + let all_stopped = self .as_local() - .is_some_and(|local| local.language_servers_stopped); - if !stopped { - self.restart_all_language_servers(cx); + .is_some_and(|local| local.all_language_servers_stopped); + if !all_stopped { + // Restart servers without clearing per-server stopped status. + // Individually-stopped servers will be skipped by the guard in + // register_buffer_with_language_servers. + let buffers = self.buffer_store.read(cx).buffers().collect(); + self.restart_language_servers_for_buffers(buffers, HashSet::default(), false, cx); } } @@ -5451,9 +5462,10 @@ impl LspStore { let Some(local) = self.as_local_mut() else { return; }; - if local.language_servers_stopped { + if local.all_language_servers_stopped { return; } + let stopped_language_servers = local.stopped_language_servers.clone(); let mut adapters = BTreeMap::default(); let get_adapter = { let languages = local.languages.clone(); @@ -5536,6 +5548,11 @@ impl LspStore { ) .collect::>(); for node in nodes { + if let Some(name) = node.name() + && stopped_language_servers.contains(&name) + { + continue; + } let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path)); @@ -10844,6 +10861,7 @@ impl LspStore { }) }) .collect(), + true, cx, ); }); @@ -11387,7 +11405,7 @@ impl LspStore { pub fn stop_all_language_servers(&mut self, cx: &mut Context) { if let Some(local) = self.as_local_mut() { - local.language_servers_stopped = true; + local.all_language_servers_stopped = true; } self.shutdown_all_language_servers(cx).detach(); } @@ -11425,16 +11443,19 @@ impl LspStore { pub fn restart_all_language_servers(&mut self, cx: &mut Context) { if let Some(local) = self.as_local_mut() { - local.language_servers_stopped = false; + local.all_language_servers_stopped = false; } + // `restart_language_servers_for_buffers` with empty selectors and `clear_stopped` + // clears `stopped_language_servers` for us. let buffers = self.buffer_store.read(cx).buffers().collect(); - self.restart_language_servers_for_buffers(buffers, HashSet::default(), cx); + self.restart_language_servers_for_buffers(buffers, HashSet::default(), true, cx); } pub fn restart_language_servers_for_buffers( &mut self, buffers: Vec>, only_restart_servers: HashSet, + clear_stopped: bool, cx: &mut Context, ) { if let Some((client, project_id)) = self.upstream_client() { @@ -11468,7 +11489,7 @@ impl LspStore { }); cx.background_spawn(request).detach_and_log_err(cx); } else { - let stop_task = if only_restart_servers.is_empty() { + let (stopped_names, stop_task) = if only_restart_servers.is_empty() { self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx) } else { self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx) @@ -11476,6 +11497,25 @@ impl LspStore { cx.spawn(async move |lsp_store, cx| { stop_task.await; lsp_store.update(cx, |lsp_store, cx| { + if clear_stopped { + if let Some(local) = lsp_store.as_local_mut() { + if only_restart_servers.is_empty() { + // A full restart of these buffers un-suppresses every + // manually-stopped server, even ones that are no longer + // running (and so weren't returned in `stopped_names`). + local.stopped_language_servers.clear(); + } else { + for name in &stopped_names { + local.stopped_language_servers.remove(name); + } + for selector in &only_restart_servers { + if let LanguageServerSelector::Name(name) = selector { + local.stopped_language_servers.remove(name); + } + } + } + } + } for buffer in buffers { lsp_store.register_buffer_with_language_servers( &buffer, @@ -11530,8 +11570,11 @@ impl LspStore { Ok(()) }) } else { - let task = + let (stopped_names, task) = self.stop_local_language_servers_for_buffers(&buffers, also_stop_servers, cx); + if let Some(local) = self.as_local_mut() { + local.stopped_language_servers.extend(stopped_names); + } cx.background_spawn(async move { task.await; Ok(()) @@ -11544,9 +11587,9 @@ impl LspStore { buffers: &[Entity], also_stop_servers: HashSet, cx: &mut Context, - ) -> Task<()> { + ) -> (HashSet, Task<()>) { let Some(local) = self.as_local_mut() else { - return Task::ready(()); + return (HashSet::default(), Task::ready(())); }; let mut language_server_names_to_stop = BTreeSet::default(); let mut language_servers_to_stop = also_stop_servers @@ -11588,13 +11631,27 @@ impl LspStore { ); } + let stopped_names: HashSet = language_servers_to_stop + .iter() + .filter_map(|id| { + local + .language_server_ids + .iter() + .find(|(_, state)| state.id == *id) + .map(|(seed, _)| seed.name.clone()) + }) + .collect(); + local.lsp_tree.remove_nodes(&language_servers_to_stop); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) .collect::>(); - cx.background_spawn(futures::future::join_all(tasks).map(|_| ())) + ( + stopped_names, + cx.background_spawn(futures::future::join_all(tasks).map(|_| ())), + ) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8544e0b833d762..cd6dc042ffb098 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3951,10 +3951,16 @@ impl Project { &mut self, buffers: Vec>, only_restart_servers: HashSet, + clear_stopped: bool, cx: &mut Context, ) { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.restart_language_servers_for_buffers(buffers, only_restart_servers, cx) + lsp_store.restart_language_servers_for_buffers( + buffers, + only_restart_servers, + clear_stopped, + cx, + ) }) } diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 9ad334ece6b0fd..9d4e88c69c13e2 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -1864,6 +1864,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { project.restart_language_servers_for_buffers( vec![rust_buffer.clone(), json_buffer.clone()], HashSet::default(), + true, cx, ); }); @@ -2981,7 +2982,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC // Restart the server before the diagnostics finish updating. project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx); + project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), true, cx); }); let mut events = cx.events(&project); @@ -3099,7 +3100,12 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx); + project.restart_language_servers_for_buffers( + vec![buffer.clone()], + HashSet::default(), + true, + cx, + ); }); // The diagnostics are cleared. @@ -3154,7 +3160,12 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T }); cx.executor().run_until_parked(); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx); + project.restart_language_servers_for_buffers( + vec![buffer.clone()], + HashSet::default(), + true, + cx, + ); }); let mut fake_server = fake_servers.next().await.unwrap(); @@ -3972,7 +3983,12 @@ async fn test_diagnostic_summaries_cleared_on_server_restart(cx: &mut gpui::Test let mut events = cx.events(&project); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx); + project.restart_language_servers_for_buffers( + vec![buffer.clone()], + HashSet::default(), + true, + cx, + ); }); cx.executor().run_until_parked();