debugger: Enable manually restarting a session when a DAP server doesn't support restarting (#28908)

Anthony Eid and Cole Miller created

This PR also fixes the unexpected behavior of clicking restart when a
session is terminated and nothing happens.

And we fixed a small bug where `DebugClientAdapter.shutdown()` was never
called.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

crates/debugger_ui/src/debugger_panel.rs | 16 ++++++++-
crates/project/src/debugger/dap_store.rs | 40 +++++++++++++++++++++++++
crates/project/src/debugger/session.rs   | 37 +++++++++++++++--------
3 files changed, 76 insertions(+), 17 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -33,6 +33,7 @@ use std::sync::Arc;
 use task::DebugTaskDefinition;
 use terminal_view::terminal_panel::TerminalPanel;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
+use util::debug_panic;
 use workspace::{
     Workspace,
     dock::{DockPosition, Panel, PanelEvent},
@@ -316,8 +317,20 @@ impl DebugPanel {
                             .any(|item| item.read(cx).session_id(cx) == session_id)
                         {
                             // We already have an item for this session.
+                            debug_panic!("We should never reuse session ids");
                             return;
                         }
+
+                        this.sessions.retain(|session| {
+                            session
+                                .read(cx)
+                                .mode()
+                                .as_running()
+                                .map_or(false, |running_state| {
+                                    !running_state.read(cx).session().read(cx).is_terminated()
+                                })
+                        });
+
                         let session_item = DebugSession::running(
                             project,
                             this.workspace.clone(),
@@ -769,9 +782,6 @@ impl DebugPanel {
                                             this.restart_session(cx);
                                         },
                                     ))
-                                    .disabled(
-                                        !capabilities.supports_restart_request.unwrap_or_default(),
-                                    )
                                     .tooltip(move |window, cx| {
                                         Tooltip::text("Restart")(window, cx)
                                     }),

crates/project/src/debugger/dap_store.rs 🔗

@@ -792,10 +792,48 @@ fn create_new_session(
         this.update(cx, |_, cx| {
             cx.subscribe(
                 &session,
-                move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
+                move |this: &mut DapStore, session, event: &SessionStateEvent, cx| match event {
                     SessionStateEvent::Shutdown => {
                         this.shutdown_session(session_id, cx).detach_and_log_err(cx);
                     }
+                    SessionStateEvent::Restart => {
+                        let Some((config, binary)) = session.read_with(cx, |session, _| {
+                            session
+                                .configuration()
+                                .map(|config| (config, session.binary().clone()))
+                        }) else {
+                            log::error!("Failed to get debug config from session");
+                            return;
+                        };
+
+                        let mut curr_session = session;
+                        while let Some(parent_id) = curr_session.read(cx).parent_id() {
+                            if let Some(parent_session) = this.sessions.get(&parent_id).cloned() {
+                                curr_session = parent_session;
+                            } else {
+                                log::error!("Failed to get parent session from parent session id");
+                                break;
+                            }
+                        }
+
+                        let session_id = curr_session.read(cx).session_id();
+
+                        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
+
+                        cx.spawn(async move |this, cx| {
+                            task.await;
+
+                            this.update(cx, |this, cx| {
+                                this.sessions.remove(&session_id);
+                                this.new_session(binary, config, None, cx)
+                            })?
+                            .1
+                            .await?;
+
+                            anyhow::Ok(())
+                        })
+                        .detach_and_log_err(cx);
+                    }
                 },
             )
             .detach();

crates/project/src/debugger/session.rs 🔗

@@ -397,6 +397,7 @@ impl LocalMode {
             self.definition.initialize_args.clone().unwrap_or(json!({})),
             &mut raw.configuration,
         );
+
         // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
         let launch = match raw.request {
             dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(
@@ -684,8 +685,9 @@ pub enum SessionEvent {
     Threads,
 }
 
-pub(crate) enum SessionStateEvent {
+pub(super) enum SessionStateEvent {
     Shutdown,
+    Restart,
 }
 
 impl EventEmitter<SessionEvent> for Session {}
@@ -1362,6 +1364,18 @@ impl Session {
         &self.loaded_sources
     }
 
+    fn fallback_to_manual_restart(
+        &mut self,
+        res: Result<()>,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        if res.log_err().is_none() {
+            cx.emit(SessionStateEvent::Restart);
+            return None;
+        }
+        Some(())
+    }
+
     fn empty_response(&mut self, res: Result<()>, _cx: &mut Context<Self>) -> Option<()> {
         res.log_err()?;
         Some(())
@@ -1421,26 +1435,17 @@ impl Session {
     }
 
     pub fn restart(&mut self, args: Option<Value>, cx: &mut Context<Self>) {
-        if self.capabilities.supports_restart_request.unwrap_or(false) {
+        if self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated() {
             self.request(
                 RestartCommand {
                     raw: args.unwrap_or(Value::Null),
                 },
-                Self::empty_response,
+                Self::fallback_to_manual_restart,
                 cx,
             )
             .detach();
         } else {
-            self.request(
-                DisconnectCommand {
-                    restart: Some(false),
-                    terminate_debuggee: Some(true),
-                    suspend_debuggee: Some(false),
-                },
-                Self::empty_response,
-                cx,
-            )
-            .detach();
+            cx.emit(SessionStateEvent::Restart);
         }
     }
 
@@ -1475,8 +1480,14 @@ impl Session {
 
         cx.emit(SessionStateEvent::Shutdown);
 
+        let debug_client = self.adapter_client();
+
         cx.background_spawn(async move {
             let _ = task.await;
+
+            if let Some(client) = debug_client {
+                client.shutdown().await.log_err();
+            }
         })
     }