Stop routing session events via the DAP store (#29588)

Conrad Irwin created

This cleans up a bunch of indirection and will make it easier to
show the session building state in the debugger terminal

Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/debugger_ui/src/debugger_panel.rs       | 167 ------------
crates/debugger_ui/src/session.rs              |  13 
crates/debugger_ui/src/session/running.rs      | 121 +++++++++
crates/debugger_ui/src/tests/debugger_panel.rs |   4 
crates/project/src/debugger/dap_store.rs       | 250 -------------------
crates/project/src/debugger/session.rs         | 137 ++++++++++
6 files changed, 267 insertions(+), 425 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -7,7 +7,6 @@ use crate::{
 };
 use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
-use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
 use dap::DebugRequest;
 use dap::{
@@ -15,7 +14,6 @@ use dap::{
     client::SessionId, debugger_settings::DebuggerSettings,
 };
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
-use futures::{SinkExt as _, channel::mpsc};
 use gpui::{
     Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
     FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
@@ -24,21 +22,11 @@ use gpui::{
 
 use language::Buffer;
 use project::debugger::session::{Session, SessionStateEvent};
-use project::{
-    Project,
-    debugger::{
-        dap_store::{self, DapStore},
-        session::ThreadStatus,
-    },
-    terminals::TerminalKind,
-};
+use project::{Project, debugger::session::ThreadStatus};
 use rpc::proto::{self};
 use settings::Settings;
 use std::any::TypeId;
-use std::path::Path;
-use std::sync::Arc;
-use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId};
-use terminal_view::TerminalView;
+use task::{DebugScenario, TaskContext};
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::SplitDirection;
 use workspace::{
@@ -74,27 +62,21 @@ pub struct DebugPanel {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
-    _subscriptions: Vec<Subscription>,
 }
 
 impl DebugPanel {
     pub fn new(
         workspace: &Workspace,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
         cx.new(|cx| {
             let project = workspace.project().clone();
-            let dap_store = project.read(cx).dap_store();
-
-            let _subscriptions =
-                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 
             let debug_panel = Self {
                 size: px(300.),
                 sessions: vec![],
                 active_session: None,
-                _subscriptions,
                 past_debug_definition: None,
                 focus_handle: cx.focus_handle(),
                 project,
@@ -288,7 +270,7 @@ impl DebugPanel {
             cx.subscribe_in(
                 &session,
                 window,
-                move |_, session, event: &SessionStateEvent, window, cx| match event {
+                move |this, session, event: &SessionStateEvent, window, cx| match event {
                     SessionStateEvent::Restart => {
                         let mut curr_session = session.clone();
                         while let Some(parent_session) = curr_session
@@ -310,6 +292,9 @@ impl DebugPanel {
                         })
                         .detach_and_log_err(cx);
                     }
+                    SessionStateEvent::SpawnChildSession { request } => {
+                        this.handle_start_debugging_request(request, session.clone(), window, cx);
+                    }
                     _ => {}
                 },
             )
@@ -357,7 +342,7 @@ impl DebugPanel {
         Ok(())
     }
 
-    pub fn start_child_session(
+    pub fn handle_start_debugging_request(
         &mut self,
         request: &StartDebuggingRequestArguments,
         parent_session: Entity<Session>,
@@ -419,47 +404,6 @@ impl DebugPanel {
         self.active_session.clone()
     }
 
-    fn handle_dap_store_event(
-        &mut self,
-        _dap_store: &Entity<DapStore>,
-        event: &dap_store::DapStoreEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match event {
-            dap_store::DapStoreEvent::RunInTerminal {
-                session_id,
-                title,
-                cwd,
-                command,
-                args,
-                envs,
-                sender,
-                ..
-            } => {
-                self.handle_run_in_terminal_request(
-                    *session_id,
-                    title.clone(),
-                    cwd.clone(),
-                    command.clone(),
-                    args.clone(),
-                    envs.clone(),
-                    sender.clone(),
-                    window,
-                    cx,
-                )
-                .detach_and_log_err(cx);
-            }
-            dap_store::DapStoreEvent::SpawnChildSession {
-                request,
-                parent_session,
-            } => {
-                self.start_child_session(request, parent_session.clone(), window, cx);
-            }
-            _ => {}
-        }
-    }
-
     pub fn resolve_scenario(
         &self,
         scenario: DebugScenario,
@@ -529,101 +473,6 @@ impl DebugPanel {
         })
     }
 
-    fn handle_run_in_terminal_request(
-        &self,
-        session_id: SessionId,
-        title: Option<String>,
-        cwd: Option<Arc<Path>>,
-        command: Option<String>,
-        args: Vec<String>,
-        envs: HashMap<String, String>,
-        mut sender: mpsc::Sender<Result<u32>>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let Some(session) = self
-            .sessions
-            .iter()
-            .find(|s| s.read(cx).session_id(cx) == session_id)
-        else {
-            return Task::ready(Err(anyhow!("no session {:?} found", session_id)));
-        };
-        let running = session.read(cx).running_state();
-        let cwd = cwd.map(|p| p.to_path_buf());
-        let shell = self
-            .project
-            .read(cx)
-            .terminal_settings(&cwd, cx)
-            .shell
-            .clone();
-        let kind = if let Some(command) = command {
-            let title = title.clone().unwrap_or(command.clone());
-            TerminalKind::Task(task::SpawnInTerminal {
-                id: TaskId("debug".to_string()),
-                full_label: title.clone(),
-                label: title.clone(),
-                command: command.clone(),
-                args,
-                command_label: title.clone(),
-                cwd,
-                env: envs,
-                use_new_terminal: true,
-                allow_concurrent_runs: true,
-                reveal: RevealStrategy::NoFocus,
-                reveal_target: RevealTarget::Dock,
-                hide: HideStrategy::Never,
-                shell,
-                show_summary: false,
-                show_command: false,
-                show_rerun: false,
-            })
-        } else {
-            TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
-        };
-
-        let workspace = self.workspace.clone();
-        let project = self.project.downgrade();
-
-        let terminal_task = self.project.update(cx, |project, cx| {
-            project.create_terminal(kind, window.window_handle(), cx)
-        });
-        let terminal_task = cx.spawn_in(window, async move |_, cx| {
-            let terminal = terminal_task.await?;
-
-            let terminal_view = cx.new_window_entity(|window, cx| {
-                TerminalView::new(terminal.clone(), workspace, None, project, window, cx)
-            })?;
-
-            running.update_in(cx, |running, window, cx| {
-                running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
-                running.debug_terminal.update(cx, |debug_terminal, cx| {
-                    debug_terminal.terminal = Some(terminal_view);
-                    cx.notify();
-                });
-            })?;
-
-            anyhow::Ok(terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())?)
-        });
-
-        cx.background_spawn(async move {
-            match terminal_task.await {
-                Ok(pid_task) => match pid_task {
-                    Some(pid) => sender.send(Ok(pid.as_u32())).await?,
-                    None => {
-                        sender
-                            .send(Err(anyhow!(
-                                "Terminal was spawned but PID was not available"
-                            )))
-                            .await?
-                    }
-                },
-                Err(error) => sender.send(Err(anyhow!(error))).await?,
-            };
-
-            Ok(())
-        })
-    }
-
     fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
         let Some(session) = self
             .sessions

crates/debugger_ui/src/session.rs 🔗

@@ -104,12 +104,6 @@ impl DebugSession {
         &self.mode
     }
 
-    pub(crate) fn running_state(&self) -> Entity<RunningState> {
-        match &self.mode {
-            DebugSessionState::Running(running_state) => running_state.clone(),
-        }
-    }
-
     pub(crate) fn label(&self, cx: &App) -> SharedString {
         if let Some(label) = self.label.get() {
             return label.clone();
@@ -131,6 +125,13 @@ impl DebugSession {
             .to_owned()
     }
 
+    #[allow(unused)]
+    pub(crate) fn running_state(&self) -> Entity<RunningState> {
+        match &self.mode {
+            DebugSessionState::Running(running_state) => running_state.clone(),
+        }
+    }
+
     pub(crate) fn label_element(&self, cx: &App) -> AnyElement {
         let label = self.label(cx);
 

crates/debugger_ui/src/session/running.rs 🔗

@@ -5,15 +5,20 @@ pub(crate) mod module_list;
 pub mod stack_frame_list;
 pub mod variable_list;
 
-use std::{any::Any, ops::ControlFlow, sync::Arc, time::Duration};
+use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
 
 use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
 
 use super::DebugPanelItemEvent;
+use anyhow::{Result, anyhow};
 use breakpoint_list::BreakpointList;
 use collections::{HashMap, IndexMap};
 use console::Console;
-use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
+use dap::{
+    Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
+    debugger_settings::DebuggerSettings,
+};
+use futures::{SinkExt, channel::mpsc};
 use gpui::{
     Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
     NoAction, Pixels, Point, Subscription, Task, WeakEntity,
@@ -23,8 +28,10 @@ use module_list::ModuleList;
 use project::{
     Project,
     debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
+    terminals::TerminalKind,
 };
 use rpc::proto::ViewId;
+use serde_json::Value;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
 use terminal_view::TerminalView;
@@ -32,7 +39,7 @@ use ui::{
     ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
     DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement,
     Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement,
-    Styled, Tab, Tooltip, VisibleOnHover, Window, div, h_flex, v_flex,
+    Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
 };
 use util::ResultExt;
 use variable_list::VariableList;
@@ -559,6 +566,9 @@ impl RunningState {
                             this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
                         }
                     }
+                    SessionEvent::RunInTerminal { request, sender } => this
+                        .handle_run_in_terminal(request, sender.clone(), window, cx)
+                        .detach_and_log_err(cx),
 
                     _ => {}
                 }
@@ -657,6 +667,111 @@ impl RunningState {
         self.panes.pane_at_pixel_position(position).is_some()
     }
 
+    fn handle_run_in_terminal(
+        &self,
+        request: &RunInTerminalRequestArguments,
+        mut sender: mpsc::Sender<Result<u32>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let running = cx.entity();
+        let Ok(project) = self
+            .workspace
+            .update(cx, |workspace, _| workspace.project().clone())
+        else {
+            return Task::ready(Err(anyhow!("no workspace")));
+        };
+        let session = self.session.read(cx);
+
+        let cwd = Some(&request.cwd)
+            .filter(|cwd| cwd.len() > 0)
+            .map(PathBuf::from)
+            .or_else(|| session.binary().cwd.clone());
+
+        let mut args = request.args.clone();
+
+        // Handle special case for NodeJS debug adapter
+        // If only the Node binary path is provided, we set the command to None
+        // This prevents the NodeJS REPL from appearing, which is not the desired behavior
+        // The expected usage is for users to provide their own Node command, e.g., `node test.js`
+        // This allows the NodeJS debug client to attach correctly
+        let command = if args.len() > 1 {
+            Some(args.remove(0))
+        } else {
+            None
+        };
+
+        let mut envs: HashMap<String, String> = Default::default();
+        if let Some(Value::Object(env)) = &request.env {
+            for (key, value) in env {
+                let value_str = match (key.as_str(), value) {
+                    (_, Value::String(value)) => value,
+                    _ => continue,
+                };
+
+                envs.insert(key.clone(), value_str.clone());
+            }
+        }
+
+        let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
+        let kind = if let Some(command) = command {
+            let title = request.title.clone().unwrap_or(command.clone());
+            TerminalKind::Task(task::SpawnInTerminal {
+                id: task::TaskId("debug".to_string()),
+                full_label: title.clone(),
+                label: title.clone(),
+                command: command.clone(),
+                args,
+                command_label: title.clone(),
+                cwd,
+                env: envs,
+                use_new_terminal: true,
+                allow_concurrent_runs: true,
+                reveal: task::RevealStrategy::NoFocus,
+                reveal_target: task::RevealTarget::Dock,
+                hide: task::HideStrategy::Never,
+                shell,
+                show_summary: false,
+                show_command: false,
+                show_rerun: false,
+            })
+        } else {
+            TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
+        };
+
+        let workspace = self.workspace.clone();
+        let weak_project = project.downgrade();
+
+        let terminal_task = project.update(cx, |project, cx| {
+            project.create_terminal(kind, window.window_handle(), cx)
+        });
+        let terminal_task = cx.spawn_in(window, async move |_, cx| {
+            let terminal = terminal_task.await?;
+
+            let terminal_view = cx.new_window_entity(|window, cx| {
+                TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx)
+            })?;
+
+            running.update_in(cx, |running, window, cx| {
+                running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
+                running.debug_terminal.update(cx, |debug_terminal, cx| {
+                    debug_terminal.terminal = Some(terminal_view);
+                    cx.notify();
+                });
+            })?;
+
+            terminal.read_with(cx, |terminal, _| {
+                terminal
+                    .pty_info
+                    .pid()
+                    .map(|pid| pid.as_u32())
+                    .ok_or_else(|| anyhow!("Terminal was spawned but PID was not available"))
+            })?
+        });
+
+        cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
+    }
+
     fn create_sub_view(
         &self,
         item_kind: DebuggerPaneItem,

crates/debugger_ui/src/tests/debugger_panel.rs 🔗

@@ -523,8 +523,8 @@ async fn test_handle_error_run_in_terminal_reverse_request(
         .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
             kind: None,
             title: None,
-            cwd: path!("/non-existing/path").into(), // invalid/non-existing path will cause the terminal spawn to fail
-            args: vec![],
+            cwd: "".into(),
+            args: vec!["oops".into(), "oops".into()],
             env: None,
             args_can_be_interpreted_by_shell: None,
         })

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

@@ -15,20 +15,16 @@ use async_trait::async_trait;
 use collections::HashMap;
 use dap::{
     Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
-    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
-    Source, StackFrameId, StartDebuggingRequestArguments,
+    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
     adapters::{
         DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
     },
     client::SessionId,
     messages::Message,
-    requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
+    requests::{Completions, Evaluate},
 };
 use fs::Fs;
-use futures::{
-    channel::mpsc,
-    future::{Shared, join_all},
-};
+use futures::future::{Shared, join_all};
 use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 use http_client::HttpClient;
 use language::{
@@ -43,9 +39,8 @@ use rpc::{
     AnyProtoClient, TypedEnvelope,
     proto::{self},
 };
-use serde_json::Value;
 use settings::{Settings, WorktreeId};
-use smol::{lock::Mutex, stream::StreamExt};
+use smol::lock::Mutex;
 use std::{
     borrow::Borrow,
     collections::{BTreeMap, HashSet},
@@ -67,19 +62,6 @@ pub enum DapStoreEvent {
         session_id: SessionId,
         message: Message,
     },
-    RunInTerminal {
-        session_id: SessionId,
-        title: Option<String>,
-        cwd: Option<Arc<Path>>,
-        command: Option<String>,
-        args: Vec<String>,
-        envs: HashMap<String, String>,
-        sender: mpsc::Sender<Result<u32>>,
-    },
-    SpawnChildSession {
-        request: StartDebuggingRequestArguments,
-        parent_session: Entity<Session>,
-    },
     Notification(String),
     RemoteHasInitialized,
 }
@@ -113,8 +95,6 @@ pub struct DapStore {
     worktree_store: Entity<WorktreeStore>,
     sessions: BTreeMap<SessionId, Entity<Session>>,
     next_session_id: u32,
-    start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
-    _start_debugging_task: Task<()>,
 }
 
 impl EventEmitter<DapStoreEvent> for DapStore {}
@@ -184,35 +164,10 @@ impl DapStore {
         mode: DapStoreMode,
         breakpoint_store: Entity<BreakpointStore>,
         worktree_store: Entity<WorktreeStore>,
-        cx: &mut Context<Self>,
+        _cx: &mut Context<Self>,
     ) -> Self {
-        let (start_debugging_tx, mut message_rx) =
-            futures::channel::mpsc::unbounded::<(SessionId, Message)>();
-        let task = cx.spawn(async move |this, cx| {
-            while let Some((session_id, message)) = message_rx.next().await {
-                match message {
-                    Message::Request(request) => {
-                        let _ = this
-                            .update(cx, |this, cx| {
-                                if request.command == StartDebugging::COMMAND {
-                                    this.handle_start_debugging_request(session_id, request, cx)
-                                        .detach_and_log_err(cx);
-                                } else if request.command == RunInTerminal::COMMAND {
-                                    this.handle_run_in_terminal_request(session_id, request, cx)
-                                        .detach_and_log_err(cx);
-                                }
-                            })
-                            .log_err();
-                    }
-                    _ => {}
-                }
-            }
-        });
-
         Self {
             mode,
-            _start_debugging_task: task,
-            start_debugging_tx,
             next_session_id: 0,
             downstream_client: None,
             breakpoint_store,
@@ -450,14 +405,11 @@ impl DapStore {
             });
         }
 
-        let start_debugging_tx = self.start_debugging_tx.clone();
-
         let session = Session::new(
             self.breakpoint_store.clone(),
             session_id,
             parent_session,
             template.clone(),
-            start_debugging_tx,
             cx,
         );
 
@@ -469,7 +421,7 @@ impl DapStore {
                 SessionStateEvent::Shutdown => {
                     this.shutdown_session(session_id, cx).detach_and_log_err(cx);
                 }
-                SessionStateEvent::Restart => {}
+                SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
                 SessionStateEvent::Running => {
                     cx.emit(DapStoreEvent::DebugClientStarted(session_id));
                 }
@@ -583,196 +535,6 @@ impl DapStore {
         )
     }
 
-    fn handle_start_debugging_request(
-        &mut self,
-        session_id: SessionId,
-        request: dap::messages::Request,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let Some(parent_session) = self.session_by_id(session_id) else {
-            return Task::ready(Err(anyhow!("Session not found")));
-        };
-        let request_seq = request.seq;
-
-        let launch_request: Option<Result<StartDebuggingRequestArguments, _>> = request
-            .arguments
-            .as_ref()
-            .map(|value| serde_json::from_value(value.clone()));
-
-        let mut success = true;
-        if let Some(Ok(request)) = launch_request {
-            cx.emit(DapStoreEvent::SpawnChildSession {
-                request,
-                parent_session: parent_session.clone(),
-            });
-        } else {
-            log::error!(
-                "Failed to parse launch request arguments: {:?}",
-                request.arguments
-            );
-            success = false;
-        }
-
-        cx.spawn(async move |_, cx| {
-            parent_session
-                .update(cx, |session, cx| {
-                    session.respond_to_client(
-                        request_seq,
-                        success,
-                        StartDebugging::COMMAND.to_string(),
-                        None,
-                        cx,
-                    )
-                })?
-                .await
-        })
-    }
-
-    fn handle_run_in_terminal_request(
-        &mut self,
-        session_id: SessionId,
-        request: dap::messages::Request,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let Some(session) = self.session_by_id(session_id) else {
-            return Task::ready(Err(anyhow!("Session not found")));
-        };
-
-        let request_args = serde_json::from_value::<RunInTerminalRequestArguments>(
-            request.arguments.unwrap_or_default(),
-        )
-        .expect("To parse StartDebuggingRequestArguments");
-
-        let seq = request.seq;
-
-        let cwd = Path::new(&request_args.cwd);
-
-        match cwd.try_exists() {
-            Ok(false) | Err(_) if !request_args.cwd.is_empty() => {
-                return session.update(cx, |session, cx| {
-                    session.respond_to_client(
-                        seq,
-                        false,
-                        RunInTerminal::COMMAND.to_string(),
-                        serde_json::to_value(dap::ErrorResponse {
-                            error: Some(dap::Message {
-                                id: seq,
-                                format: format!("Received invalid/unknown cwd: {cwd:?}"),
-                                variables: None,
-                                send_telemetry: None,
-                                show_user: None,
-                                url: None,
-                                url_label: None,
-                            }),
-                        })
-                        .ok(),
-                        cx,
-                    )
-                });
-            }
-            _ => (),
-        }
-        let mut args = request_args.args.clone();
-
-        // Handle special case for NodeJS debug adapter
-        // If only the Node binary path is provided, we set the command to None
-        // This prevents the NodeJS REPL from appearing, which is not the desired behavior
-        // The expected usage is for users to provide their own Node command, e.g., `node test.js`
-        // This allows the NodeJS debug client to attach correctly
-        let command = if args.len() > 1 {
-            Some(args.remove(0))
-        } else {
-            None
-        };
-
-        let mut envs: HashMap<String, String> = Default::default();
-        if let Some(Value::Object(env)) = request_args.env {
-            for (key, value) in env {
-                let value_str = match (key.as_str(), value) {
-                    (_, Value::String(value)) => value,
-                    _ => continue,
-                };
-
-                envs.insert(key, value_str);
-            }
-        }
-
-        let (tx, mut rx) = mpsc::channel::<Result<u32>>(1);
-        let cwd = Some(cwd)
-            .filter(|cwd| cwd.as_os_str().len() > 0)
-            .map(Arc::from)
-            .or_else(|| {
-                self.session_by_id(session_id)
-                    .and_then(|session| session.read(cx).binary().cwd.as_deref().map(Arc::from))
-            });
-        cx.emit(DapStoreEvent::RunInTerminal {
-            session_id,
-            title: request_args.title,
-            cwd,
-            command,
-            args,
-            envs,
-            sender: tx,
-        });
-        cx.notify();
-
-        let session = session.downgrade();
-        cx.spawn(async move |_, cx| {
-            let (success, body) = match rx.next().await {
-                Some(Ok(pid)) => (
-                    true,
-                    serde_json::to_value(dap::RunInTerminalResponse {
-                        process_id: None,
-                        shell_process_id: Some(pid as u64),
-                    })
-                    .ok(),
-                ),
-                Some(Err(error)) => (
-                    false,
-                    serde_json::to_value(dap::ErrorResponse {
-                        error: Some(dap::Message {
-                            id: seq,
-                            format: error.to_string(),
-                            variables: None,
-                            send_telemetry: None,
-                            show_user: None,
-                            url: None,
-                            url_label: None,
-                        }),
-                    })
-                    .ok(),
-                ),
-                None => (
-                    false,
-                    serde_json::to_value(dap::ErrorResponse {
-                        error: Some(dap::Message {
-                            id: seq,
-                            format: "failed to receive response from spawn terminal".to_string(),
-                            variables: None,
-                            send_telemetry: None,
-                            show_user: None,
-                            url: None,
-                            url_label: None,
-                        }),
-                    })
-                    .ok(),
-                ),
-            };
-
-            session
-                .update(cx, |session, cx| {
-                    session.respond_to_client(
-                        seq,
-                        success,
-                        RunInTerminal::COMMAND.to_string(),
-                        body,
-                        cx,
-                    )
-                })?
-                .await
-        })
-    }
-
     pub fn evaluate(
         &self,
         session_id: &SessionId,

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

@@ -14,14 +14,18 @@ use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet, IndexMap, IndexSet};
 use dap::adapters::{DebugAdapterBinary, DebugTaskDefinition};
 use dap::messages::Response;
+use dap::requests::{Request, RunInTerminal, StartDebugging};
 use dap::{
     Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
     SteppingGranularity, StoppedEvent, VariableReference,
     client::{DebugAdapterClient, SessionId},
     messages::{Events, Message},
 };
-use dap::{ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory};
-use futures::channel::oneshot;
+use dap::{
+    ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory,
+    RunInTerminalRequestArguments, StartDebuggingRequestArguments,
+};
+use futures::channel::{mpsc, oneshot};
 use futures::{FutureExt, future::Shared};
 use gpui::{
     App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
@@ -522,7 +526,6 @@ pub struct Session {
     is_session_terminated: bool,
     requests: HashMap<TypeId, HashMap<RequestSlot, Shared<Task<Option<()>>>>>,
     exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
-    start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
     background_tasks: Vec<Task<()>>,
 }
 
@@ -608,13 +611,20 @@ pub enum SessionEvent {
     Threads,
     InvalidateInlineValue,
     CapabilitiesLoaded,
+    RunInTerminal {
+        request: RunInTerminalRequestArguments,
+        sender: mpsc::Sender<Result<u32>>,
+    },
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum SessionStateEvent {
     Running,
     Shutdown,
     Restart,
+    SpawnChildSession {
+        request: StartDebuggingRequestArguments,
+    },
 }
 
 impl EventEmitter<SessionEvent> for Session {}
@@ -629,7 +639,6 @@ impl Session {
         session_id: SessionId,
         parent_session: Option<Entity<Session>>,
         template: DebugTaskDefinition,
-        start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new::<Self>(|cx| {
@@ -678,7 +687,6 @@ impl Session {
                 is_session_terminated: false,
                 exception_breakpoints: Default::default(),
                 definition: template,
-                start_debugging_requests_tx,
             };
 
             this
@@ -702,7 +710,6 @@ impl Session {
     ) -> Task<Result<()>> {
         let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded();
         let (initialized_tx, initialized_rx) = futures::channel::oneshot::channel();
-        let session_id = self.session_id();
 
         let background_tasks = vec![cx.spawn(async move |this: WeakEntity<Session>, cx| {
             let mut initialized_tx = Some(initialized_tx);
@@ -719,10 +726,15 @@ impl Session {
                             break;
                         };
                     }
-                } else {
-                    let Ok(Ok(_)) = this.update(cx, |this, _| {
-                        this.start_debugging_requests_tx
-                            .unbounded_send((session_id, message))
+                } else if let Message::Request(request) = message {
+                    let Ok(_) = this.update(cx, |this, cx| {
+                        if request.command == StartDebugging::COMMAND {
+                            this.handle_start_debugging_request(request, cx)
+                                .detach_and_log_err(cx);
+                        } else if request.command == RunInTerminal::COMMAND {
+                            this.handle_run_in_terminal_request(request, cx)
+                                .detach_and_log_err(cx);
+                        }
                     }) else {
                         break;
                     };
@@ -830,6 +842,109 @@ impl Session {
         }
     }
 
+    fn handle_start_debugging_request(
+        &mut self,
+        request: dap::messages::Request,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let request_seq = request.seq;
+
+        let launch_request: Option<Result<StartDebuggingRequestArguments, _>> = request
+            .arguments
+            .as_ref()
+            .map(|value| serde_json::from_value(value.clone()));
+
+        let mut success = true;
+        if let Some(Ok(request)) = launch_request {
+            cx.emit(SessionStateEvent::SpawnChildSession { request });
+        } else {
+            log::error!(
+                "Failed to parse launch request arguments: {:?}",
+                request.arguments
+            );
+            success = false;
+        }
+
+        cx.spawn(async move |this, cx| {
+            this.update(cx, |this, cx| {
+                this.respond_to_client(
+                    request_seq,
+                    success,
+                    StartDebugging::COMMAND.to_string(),
+                    None,
+                    cx,
+                )
+            })?
+            .await
+        })
+    }
+
+    fn handle_run_in_terminal_request(
+        &mut self,
+        request: dap::messages::Request,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let request_args = serde_json::from_value::<RunInTerminalRequestArguments>(
+            request.arguments.unwrap_or_default(),
+        )
+        .expect("To parse StartDebuggingRequestArguments");
+
+        let seq = request.seq;
+
+        let (tx, mut rx) = mpsc::channel::<Result<u32>>(1);
+        cx.emit(SessionEvent::RunInTerminal {
+            request: request_args,
+            sender: tx,
+        });
+        cx.notify();
+
+        cx.spawn(async move |session, cx| {
+            let result = util::maybe!(async move {
+                rx.next().await.ok_or_else(|| {
+                    anyhow!("failed to receive response from spawn terminal".to_string())
+                })?
+            })
+            .await;
+            let (success, body) = match result {
+                Ok(pid) => (
+                    true,
+                    serde_json::to_value(dap::RunInTerminalResponse {
+                        process_id: None,
+                        shell_process_id: Some(pid as u64),
+                    })
+                    .ok(),
+                ),
+                Err(error) => (
+                    false,
+                    serde_json::to_value(dap::ErrorResponse {
+                        error: Some(dap::Message {
+                            id: seq,
+                            format: error.to_string(),
+                            variables: None,
+                            send_telemetry: None,
+                            show_user: None,
+                            url: None,
+                            url_label: None,
+                        }),
+                    })
+                    .ok(),
+                ),
+            };
+
+            session
+                .update(cx, |session, cx| {
+                    session.respond_to_client(
+                        seq,
+                        success,
+                        RunInTerminal::COMMAND.to_string(),
+                        body,
+                        cx,
+                    )
+                })?
+                .await
+        })
+    }
+
     pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
         let adapter_id = String::from(self.definition.adapter.clone());
         let request = Initialize { adapter_id };