Handle project's node processes when releasing a Project

Anthony Eid created

Change summary

crates/copilot/src/copilot.rs                     |  2 
crates/edit_prediction/src/edit_prediction.rs     |  8 ++
crates/project/src/context_server_store.rs        |  7 ++
crates/project/src/lsp_store.rs                   | 17 ++++
crates/project/src/project.rs                     | 22 ++++++
crates/project/tests/integration/project_tests.rs | 58 +++++++++++++++++
6 files changed, 110 insertions(+), 4 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -433,7 +433,7 @@ impl Copilot {
         this
     }
 
-    fn shutdown_language_server(
+    pub fn shutdown_language_server(
         &mut self,
         _cx: &mut Context<Self>,
     ) -> impl Future<Output = ()> + use<> {

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -1037,7 +1037,13 @@ impl EditPredictionStore {
                 _subscriptions: [
                     cx.subscribe(&project, Self::handle_project_event),
                     cx.observe_release(&project, move |this, _, cx| {
-                        this.projects.remove(&entity_id);
+                        if let Some(state) = this.projects.remove(&entity_id) {
+                            if let Some(copilot) = state.copilot {
+                                let shutdown = copilot
+                                    .update(cx, |copilot, cx| copilot.shutdown_language_server(cx));
+                                cx.background_spawn(shutdown).detach();
+                            }
+                        }
                         cx.notify();
                     }),
                 ],

crates/project/src/context_server_store.rs 🔗

@@ -560,6 +560,13 @@ impl ContextServerStore {
         result
     }
 
+    pub fn stop_all_servers(&mut self, cx: &mut Context<Self>) {
+        let server_ids: Vec<_> = self.servers.keys().cloned().collect();
+        for id in server_ids {
+            self.stop_server(&id, cx).log_err();
+        }
+    }
+
     fn run_server(
         &mut self,
         server: Arc<ContextServer>,

crates/project/src/lsp_store.rs 🔗

@@ -1310,15 +1310,22 @@ impl LocalLspStore {
         clangd_ext::register_notifications(lsp_store, language_server, adapter);
     }
 
-    fn shutdown_language_servers_on_quit(&mut self) -> impl Future<Output = ()> + use<> {
+    pub(crate) fn shutdown_language_servers_on_quit(&mut self) -> impl Future<Output = ()> + use<> {
         let shutdown_futures = self
             .language_servers
             .drain()
             .map(|(_, server_state)| Self::shutdown_server(server_state))
             .collect::<Vec<_>>();
 
+        let supplementary_shutdown_futures = self
+            .supplementary_language_servers
+            .drain()
+            .filter_map(|(_, (_, server))| server.shutdown())
+            .collect::<Vec<_>>();
+
         async move {
             join_all(shutdown_futures).await;
+            join_all(supplementary_shutdown_futures).await;
         }
     }
 
@@ -10858,12 +10865,20 @@ impl LspStore {
                 .map(|state| state.id)
                 .collect();
             local.lsp_tree.remove_nodes(&language_servers_to_stop);
+
+            let supplementary_shutdown_futures: Vec<_> = local
+                .supplementary_language_servers
+                .drain()
+                .filter_map(|(_, (_, server))| server.shutdown())
+                .collect();
+
             let tasks = language_servers_to_stop
                 .into_iter()
                 .map(|server| self.stop_local_language_server(server, cx))
                 .collect::<Vec<_>>();
             cx.background_spawn(async move {
                 futures::future::join_all(tasks).await;
+                futures::future::join_all(supplementary_shutdown_futures).await;
             })
         }
     }

crates/project/src/project.rs 🔗

@@ -1928,9 +1928,29 @@ impl Project {
             .detach()
         }
 
+        let process_shutdown = |this: &mut Project, cx: &mut App| {
+            this.lsp_store.update(cx, |lsp_store, cx| {
+                if let Some(local) = lsp_store.as_local_mut() {
+                    let shutdown = local.shutdown_language_servers_on_quit();
+                    cx.background_spawn(shutdown).detach();
+                }
+            });
+
+            this.dap_store.update(cx, |dap_store, cx| {
+                dap_store.shutdown_sessions(cx).detach();
+            });
+
+            this.context_server_store.update(cx, |store, cx| {
+                store.stop_all_servers(cx);
+            });
+        };
+
         match &self.client_state {
-            ProjectClientState::Local => {}
+            ProjectClientState::Local => {
+                process_shutdown(self, cx);
+            }
             ProjectClientState::Shared { .. } => {
+                process_shutdown(self, cx);
                 let _ = self.unshare_internal(cx);
             }
             ProjectClientState::Remote { remote_id, .. } => {

crates/project/tests/integration/project_tests.rs 🔗

@@ -11320,6 +11320,64 @@ async fn test_undo_encoding_change(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_language_servers_shut_down_on_project_release(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/project"),
+        json!({
+            ".git": {},
+            "main.rs": "fn main() {}"
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, [path!("/project").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!("/project/main.rs"), cx)
+        })
+        .await
+        .unwrap();
+
+    let mut fake_server = fake_servers.next().await.unwrap();
+    cx.executor().run_until_parked();
+
+    // Verify the server is running.
+    project.read_with(cx, |project, cx| {
+        assert!(
+            project.language_server_statuses(cx).count() > 0,
+            "language server should be running"
+        );
+    });
+
+    let held_server = project.read_with(cx, |project, cx| {
+        let lsp_store = project.lsp_store().read(cx);
+        let (server_id, _status) = lsp_store.language_server_statuses().next().unwrap();
+        lsp_store.language_server_for_id(server_id).unwrap()
+    });
+
+    drop(_handle);
+    drop(buffer);
+    drop(project);
+
+    cx.update(|_| {});
+    cx.executor().run_until_parked();
+
+    fake_server
+        .receive_notification::<lsp::notification::Exit>()
+        .await;
+
+    drop(held_server);
+}
+
 pub fn init_test(cx: &mut gpui::TestAppContext) {
     zlog::init_test();