debugger: Open debugger panel on session startup (#29186)

Conrad Irwin , Anthony Eid , Anthony , Cole Miller , Cole Miller , Zed AI , and Remco Smits created

Now all debug sessions are routed through the debug panel and are
started synchronously instead of by a task that returns a session once
the initialization process is finished. A session is `Mode::Booting`
while it's starting the debug adapter process and then transitions to
`Mode::Running` once this is completed.

This PR also added new tests for the dap logger, reverse start debugging
request, and debugging over SSH.

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Zed AI <ai@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

Cargo.lock                                                    |   3 
crates/collab/Cargo.toml                                      |   1 
crates/collab/src/tests/debug_panel_tests.rs                  |   6 
crates/collab/src/tests/remote_editing_collaboration_tests.rs | 108 +
crates/dap/src/transport.rs                                   |  59 
crates/debugger_tools/Cargo.toml                              |   3 
crates/debugger_tools/src/dap_log.rs                          |  30 
crates/debugger_ui/Cargo.toml                                 |   8 
crates/debugger_ui/src/attach_modal.rs                        |  41 
crates/debugger_ui/src/debugger_panel.rs                      | 322 ++
crates/debugger_ui/src/debugger_ui.rs                         |   2 
crates/debugger_ui/src/new_session_modal.rs                   |  79 
crates/debugger_ui/src/session.rs                             |  14 
crates/debugger_ui/src/session/running.rs                     | 102 
crates/debugger_ui/src/tests.rs                               |  60 
crates/debugger_ui/src/tests/attach_modal.rs                  |  10 
crates/debugger_ui/src/tests/console.rs                       |   9 
crates/debugger_ui/src/tests/dap_logger.rs                    | 118 +
crates/debugger_ui/src/tests/debugger_panel.rs                | 131 
crates/debugger_ui/src/tests/module_list.rs                   |  10 
crates/debugger_ui/src/tests/stack_frame_list.rs              |  16 
crates/debugger_ui/src/tests/variable_list.rs                 |  24 
crates/project/src/debugger/dap_store.rs                      | 405 +--
crates/project/src/debugger/session.rs                        | 517 ++--
crates/project/src/debugger/test.rs                           |  71 
crates/project/src/project.rs                                 |  87 
crates/workspace/src/tasks.rs                                 |  20 
crates/workspace/src/workspace.rs                             |  12 
crates/zed/src/zed.rs                                         |   2 
29 files changed, 1,255 insertions(+), 1,015 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3042,6 +3042,7 @@ dependencies = [
  "strum 0.27.1",
  "subtle",
  "supermaven_api",
+ "task",
  "telemetry_events",
  "text",
  "theme",
@@ -4189,6 +4190,7 @@ dependencies = [
  "command_palette_hooks",
  "dap",
  "db",
+ "debugger_tools",
  "editor",
  "env_logger 0.11.8",
  "feature_flags",
@@ -4198,6 +4200,7 @@ dependencies = [
  "language",
  "log",
  "menu",
+ "parking_lot",
  "picker",
  "pretty_assertions",
  "project",

crates/collab/Cargo.toml 🔗

@@ -128,6 +128,7 @@ serde_json.workspace = true
 session = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 sqlx = { version = "0.8", features = ["sqlite"] }
+task.workspace = true
 theme.workspace = true
 unindent.workspace = true
 util.workspace = true

crates/collab/src/tests/debug_panel_tests.rs 🔗

@@ -1,7 +1,7 @@
 use call::ActiveCall;
-use dap::requests::{Initialize, Launch, StackTrace};
 use dap::DebugRequestType;
-use dap::{requests::SetBreakpoints, SourceBreakpoint};
+use dap::requests::{Initialize, Launch, StackTrace};
+use dap::{SourceBreakpoint, requests::SetBreakpoints};
 use debugger_ui::debugger_panel::DebugPanel;
 use debugger_ui::session::DebugSession;
 use editor::Editor;
@@ -13,7 +13,7 @@ use std::{
     path::Path,
     sync::atomic::{AtomicBool, Ordering},
 };
-use workspace::{dock::Panel, Workspace};
+use workspace::{Workspace, dock::Panel};
 
 use super::{TestClient, TestServer};
 

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -2,11 +2,13 @@ use crate::tests::TestServer;
 use call::ActiveCall;
 use collections::{HashMap, HashSet};
 
+use debugger_ui::debugger_panel::DebugPanel;
 use extension::ExtensionHostProxy;
 use fs::{FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
 use gpui::{
     AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
+    VisualContext,
 };
 use http_client::BlockedHttpClient;
 use language::{
@@ -24,6 +26,7 @@ use project::{
 };
 use remote::SshRemoteClient;
 use remote_server::{HeadlessAppState, HeadlessProject};
+use rpc::proto;
 use serde_json::json;
 use settings::SettingsStore;
 use std::{path::Path, sync::Arc};
@@ -576,3 +579,108 @@ async fn test_ssh_collaboration_formatting_with_prettier(
         "Prettier formatting was not applied to client buffer after host's request"
     );
 }
+
+#[gpui::test]
+async fn test_remote_server_debugger(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
+    cx_a.update(|cx| {
+        release_channel::init(SemanticVersion::default(), cx);
+        command_palette_hooks::init(cx);
+        if std::env::var("RUST_LOG").is_ok() {
+            env_logger::try_init().ok();
+        }
+    });
+    server_cx.update(|cx| {
+        release_channel::init(SemanticVersion::default(), cx);
+    });
+    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+    let remote_fs = FakeFs::new(server_cx.executor());
+    remote_fs
+        .insert_tree(
+            path!("/code"),
+            json!({
+                "lib.rs": "fn one() -> usize { 1 }"
+            }),
+        )
+        .await;
+
+    // User A connects to the remote project via SSH.
+    server_cx.update(HeadlessProject::init);
+    let remote_http_client = Arc::new(BlockedHttpClient);
+    let node = NodeRuntime::unavailable();
+    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+    let _headless_project = server_cx.new(|cx| {
+        client::init_settings(cx);
+        HeadlessProject::new(
+            HeadlessAppState {
+                session: server_ssh,
+                fs: remote_fs.clone(),
+                http_client: remote_http_client,
+                node_runtime: node,
+                languages,
+                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
+            },
+            cx,
+        )
+    });
+
+    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+    let mut server = TestServer::start(server_cx.executor()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    cx_a.update(|cx| {
+        debugger_ui::init(cx);
+        command_palette_hooks::init(cx);
+    });
+    let (project_a, _) = client_a
+        .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
+        .await;
+
+    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
+
+    let debugger_panel = workspace
+        .update_in(cx_a, |_workspace, window, cx| {
+            cx.spawn_in(window, DebugPanel::load)
+        })
+        .await
+        .unwrap();
+
+    workspace.update_in(cx_a, |workspace, window, cx| {
+        workspace.add_panel(debugger_panel, window, cx);
+    });
+
+    cx_a.run_until_parked();
+    let debug_panel = workspace
+        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+        .unwrap();
+
+    let workspace_window = cx_a
+        .window_handle()
+        .downcast::<workspace::Workspace>()
+        .unwrap();
+
+    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
+    cx_a.run_until_parked();
+    debug_panel.update(cx_a, |debug_panel, cx| {
+        assert_eq!(
+            debug_panel.active_session().unwrap().read(cx).session(cx),
+            session
+        )
+    });
+
+    session.update(cx_a, |session, _| {
+        assert_eq!(session.binary().command, "ssh");
+    });
+
+    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
+        workspace.project().update(cx, |project, cx| {
+            project.dap_store().update(cx, |dap_store, cx| {
+                dap_store.shutdown_session(session.read(cx).session_id(), cx)
+            })
+        })
+    });
+
+    client_ssh.update(cx_a, |a, _| {
+        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}))
+    });
+
+    shutdown_session.await.unwrap();
+}

crates/dap/src/transport.rs 🔗

@@ -22,7 +22,7 @@ use std::{
     time::Duration,
 };
 use task::TcpArgumentsTemplate;
-use util::ResultExt as _;
+use util::{ResultExt as _, TryFutureExt};
 
 use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
 
@@ -126,6 +126,7 @@ pub(crate) struct TransportDelegate {
     pending_requests: Requests,
     transport: Transport,
     server_tx: Arc<Mutex<Option<Sender<Message>>>>,
+    _tasks: Vec<gpui::Task<Option<()>>>,
 }
 
 impl TransportDelegate {
@@ -140,6 +141,7 @@ impl TransportDelegate {
             log_handlers: Default::default(),
             current_requests: Default::default(),
             pending_requests: Default::default(),
+            _tasks: Default::default(),
         };
         let messages = this.start_handlers(transport_pipes, cx).await?;
         Ok((messages, this))
@@ -166,35 +168,43 @@ impl TransportDelegate {
 
         cx.update(|cx| {
             if let Some(stdout) = params.stdout.take() {
-                cx.background_executor()
-                    .spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
-                    .detach_and_log_err(cx);
+                self._tasks.push(
+                    cx.background_executor()
+                        .spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
+                );
             }
 
-            cx.background_executor()
-                .spawn(Self::handle_output(
-                    params.output,
-                    client_tx,
-                    self.pending_requests.clone(),
-                    log_handler.clone(),
-                ))
-                .detach_and_log_err(cx);
+            self._tasks.push(
+                cx.background_executor().spawn(
+                    Self::handle_output(
+                        params.output,
+                        client_tx,
+                        self.pending_requests.clone(),
+                        log_handler.clone(),
+                    )
+                    .log_err(),
+                ),
+            );
 
             if let Some(stderr) = params.stderr.take() {
-                cx.background_executor()
-                    .spawn(Self::handle_error(stderr, self.log_handlers.clone()))
-                    .detach_and_log_err(cx);
+                self._tasks.push(
+                    cx.background_executor()
+                        .spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
+                );
             }
 
-            cx.background_executor()
-                .spawn(Self::handle_input(
-                    params.input,
-                    client_rx,
-                    self.current_requests.clone(),
-                    self.pending_requests.clone(),
-                    log_handler.clone(),
-                ))
-                .detach_and_log_err(cx);
+            self._tasks.push(
+                cx.background_executor().spawn(
+                    Self::handle_input(
+                        params.input,
+                        client_rx,
+                        self.current_requests.clone(),
+                        self.pending_requests.clone(),
+                        log_handler.clone(),
+                    )
+                    .log_err(),
+                ),
+            );
         })?;
 
         {
@@ -367,6 +377,7 @@ impl TransportDelegate {
     where
         Stderr: AsyncRead + Unpin + Send + 'static,
     {
+        log::debug!("Handle error started");
         let mut buffer = String::new();
 
         let mut reader = BufReader::new(stderr);

crates/debugger_tools/Cargo.toml 🔗

@@ -12,6 +12,9 @@ workspace = true
 path = "src/debugger_tools.rs"
 doctest = false
 
+[features]
+test-support = []
+
 [dependencies]
 anyhow.workspace = true
 dap.workspace = true

crates/debugger_tools/src/dap_log.rs 🔗

@@ -41,7 +41,7 @@ struct DapLogView {
     _subscriptions: Vec<Subscription>,
 }
 
-struct LogStore {
+pub struct LogStore {
     projects: HashMap<WeakEntity<Project>, ProjectState>,
     debug_clients: HashMap<SessionId, DebugAdapterState>,
     rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
@@ -101,7 +101,7 @@ impl DebugAdapterState {
 }
 
 impl LogStore {
-    fn new(cx: &Context<Self>) -> Self {
+    pub fn new(cx: &Context<Self>) -> Self {
         let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
         cx.spawn(async move |this, cx| {
             while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
@@ -845,3 +845,29 @@ impl EventEmitter<Event> for LogStore {}
 impl EventEmitter<Event> for DapLogView {}
 impl EventEmitter<EditorEvent> for DapLogView {}
 impl EventEmitter<SearchEvent> for DapLogView {}
+
+#[cfg(any(test, feature = "test-support"))]
+impl LogStore {
+    pub fn contained_session_ids(&self) -> Vec<SessionId> {
+        self.debug_clients.keys().cloned().collect()
+    }
+
+    pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
+        self.debug_clients
+            .get(&session_id)
+            .expect("This session should exist if a test is calling")
+            .rpc_messages
+            .messages
+            .clone()
+            .into()
+    }
+
+    pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
+        self.debug_clients
+            .get(&session_id)
+            .expect("This session should exist if a test is calling")
+            .log_messages
+            .clone()
+            .into()
+    }
+}

crates/debugger_ui/Cargo.toml 🔗

@@ -20,6 +20,9 @@ test-support = [
     "project/test-support",
     "util/test-support",
     "workspace/test-support",
+    "env_logger",
+    "unindent",
+    "debugger_tools"
 ]
 
 [dependencies]
@@ -37,6 +40,7 @@ gpui.workspace = true
 language.workspace = true
 log.workspace = true
 menu.workspace = true
+parking_lot.workspace = true
 picker.workspace = true
 pretty_assertions.workspace = true
 project.workspace = true
@@ -53,9 +57,13 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
+env_logger = { workspace = true, optional = true }
+debugger_tools = { workspace = true, optional = true }
+unindent = { workspace = true, optional = true }
 
 [dev-dependencies]
 dap = { workspace = true, features = ["test-support"] }
+debugger_tools = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
 gpui = { workspace = true, features = ["test-support"] }

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -1,7 +1,7 @@
 use dap::DebugRequest;
 use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::Subscription;
 use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
+use gpui::{Subscription, WeakEntity};
 use picker::{Picker, PickerDelegate};
 
 use std::sync::Arc;
@@ -9,7 +9,9 @@ use sysinfo::System;
 use ui::{Context, Tooltip, prelude::*};
 use ui::{ListItem, ListItemSpacing};
 use util::debug_panic;
-use workspace::ModalView;
+use workspace::{ModalView, Workspace};
+
+use crate::debugger_panel::DebugPanel;
 
 #[derive(Debug, Clone)]
 pub(super) struct Candidate {
@@ -22,19 +24,19 @@ pub(crate) struct AttachModalDelegate {
     selected_index: usize,
     matches: Vec<StringMatch>,
     placeholder_text: Arc<str>,
-    project: Entity<project::Project>,
+    workspace: WeakEntity<Workspace>,
     pub(crate) debug_config: task::DebugTaskDefinition,
     candidates: Arc<[Candidate]>,
 }
 
 impl AttachModalDelegate {
     fn new(
-        project: Entity<project::Project>,
+        workspace: Entity<Workspace>,
         debug_config: task::DebugTaskDefinition,
         candidates: Arc<[Candidate]>,
     ) -> Self {
         Self {
-            project,
+            workspace: workspace.downgrade(),
             debug_config,
             candidates,
             selected_index: 0,
@@ -51,7 +53,7 @@ pub struct AttachModal {
 
 impl AttachModal {
     pub fn new(
-        project: Entity<project::Project>,
+        workspace: Entity<Workspace>,
         debug_config: task::DebugTaskDefinition,
         modal: bool,
         window: &mut Window,
@@ -75,11 +77,11 @@ impl AttachModal {
             .collect();
         processes.sort_by_key(|k| k.name.clone());
         let processes = processes.into_iter().collect();
-        Self::with_processes(project, debug_config, processes, modal, window, cx)
+        Self::with_processes(workspace, debug_config, processes, modal, window, cx)
     }
 
     pub(super) fn with_processes(
-        project: Entity<project::Project>,
+        workspace: Entity<Workspace>,
         debug_config: task::DebugTaskDefinition,
         processes: Arc<[Candidate]>,
         modal: bool,
@@ -88,7 +90,7 @@ impl AttachModal {
     ) -> Self {
         let picker = cx.new(|cx| {
             Picker::uniform_list(
-                AttachModalDelegate::new(project, debug_config, processes),
+                AttachModalDelegate::new(workspace, debug_config, processes),
                 window,
                 cx,
             )
@@ -202,7 +204,7 @@ impl PickerDelegate for AttachModalDelegate {
         })
     }
 
-    fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
         let candidate = self
             .matches
             .get(self.selected_index())
@@ -225,14 +227,17 @@ impl PickerDelegate for AttachModalDelegate {
             }
         }
 
-        let config = self.debug_config.clone();
-        self.project
-            .update(cx, |project, cx| {
-                let ret = project.start_debug_session(config, cx);
-                ret
-            })
-            .detach_and_log_err(cx);
-
+        let definition = self.debug_config.clone();
+        let panel = self
+            .workspace
+            .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+            .ok()
+            .flatten();
+        if let Some(panel) = panel {
+            panel.update(cx, |panel, cx| {
+                panel.start_session(definition, window, cx);
+            });
+        }
         cx.emit(DismissEvent);
     }
 

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -6,6 +6,7 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
 use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
+use dap::StartDebuggingRequestArguments;
 use dap::{
     ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
     client::SessionId, debugger_settings::DebuggerSettings,
@@ -17,6 +18,7 @@ use gpui::{
     actions, anchored, deferred,
 };
 
+use project::debugger::session::{Session, SessionStateEvent};
 use project::{
     Project,
     debugger::{
@@ -30,10 +32,9 @@ use settings::Settings;
 use std::any::TypeId;
 use std::path::Path;
 use std::sync::Arc;
-use task::DebugTaskDefinition;
+use task::{DebugTaskDefinition, DebugTaskTemplate};
 use terminal_view::terminal_panel::TerminalPanel;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
-use util::debug_panic;
 use workspace::{
     Workspace,
     dock::{DockPosition, Panel, PanelEvent},
@@ -63,7 +64,7 @@ pub struct DebugPanel {
     active_session: Option<Entity<DebugSession>>,
     /// This represents the last debug definition that was created in the new session modal
     pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
-    project: WeakEntity<Project>,
+    project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
@@ -97,10 +98,10 @@ impl DebugPanel {
                             window,
                             |panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
                                 panel.workspace.update(cx, |workspace, cx| {
-                                    let project = workspace.project().clone();
+                                    let workspace_handle = cx.entity().clone();
                                     workspace.toggle_modal(window, cx, |window, cx| {
                                         crate::attach_modal::AttachModal::new(
-                                            project,
+                                            workspace_handle,
                                             event.debug_config.clone(),
                                             true,
                                             window,
@@ -127,7 +128,7 @@ impl DebugPanel {
                 _subscriptions,
                 past_debug_definition: None,
                 focus_handle: cx.focus_handle(),
-                project: project.downgrade(),
+                project,
                 workspace: workspace.weak_handle(),
                 context_menu: None,
             };
@@ -219,7 +220,7 @@ impl DebugPanel {
 
     pub fn load(
         workspace: WeakEntity<Workspace>,
-        cx: AsyncWindowContext,
+        cx: &mut AsyncWindowContext,
     ) -> Task<Result<Entity<Self>>> {
         cx.spawn(async move |cx| {
             workspace.update_in(cx, |workspace, window, cx| {
@@ -245,114 +246,226 @@ impl DebugPanel {
                     });
                 })
                 .detach();
+                workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
 
                 debug_panel
             })
         })
     }
 
-    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
-        self.active_session.clone()
-    }
-
-    pub fn debug_panel_items_by_client(
-        &self,
-        client_id: &SessionId,
-        cx: &Context<Self>,
-    ) -> Vec<Entity<DebugSession>> {
-        self.sessions
-            .iter()
-            .filter(|item| item.read(cx).session_id(cx) == *client_id)
-            .map(|item| item.clone())
-            .collect()
-    }
-
-    pub fn debug_panel_item_by_client(
-        &self,
-        client_id: SessionId,
-        cx: &mut Context<Self>,
-    ) -> Option<Entity<DebugSession>> {
-        self.sessions
-            .iter()
-            .find(|item| {
-                let item = item.read(cx);
-
-                item.session_id(cx) == client_id
-            })
-            .cloned()
-    }
-
-    fn handle_dap_store_event(
+    pub fn start_session(
         &mut self,
-        dap_store: &Entity<DapStore>,
-        event: &dap_store::DapStoreEvent,
+        definition: DebugTaskDefinition,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        match event {
-            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
-                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
-                    return log::error!(
-                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
-                    );
+        let task_contexts = self
+            .workspace
+            .update(cx, |workspace, cx| {
+                tasks_ui::task_contexts(workspace, window, cx)
+            })
+            .ok();
+        let dap_store = self.project.read(cx).dap_store().clone();
+
+        cx.spawn_in(window, async move |this, cx| {
+            let task_context = if let Some(task) = task_contexts {
+                task.await
+                    .active_worktree_context
+                    .map_or(task::TaskContext::default(), |context| context.1)
+            } else {
+                task::TaskContext::default()
+            };
+
+            let (session, task) = dap_store.update(cx, |dap_store, cx| {
+                let template = DebugTaskTemplate {
+                    locator: None,
+                    definition: definition.clone(),
+                };
+                let session = if let Some(debug_config) = template
+                    .to_zed_format()
+                    .resolve_task("debug_task", &task_context)
+                    .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
+                {
+                    dap_store.new_session(debug_config.definition, None, cx)
+                } else {
+                    dap_store.new_session(definition.clone(), None, cx)
                 };
 
-                let adapter_name = session.read(cx).adapter_name();
+                (session.clone(), dap_store.boot_session(session, cx))
+            })?;
 
-                let session_id = *session_id;
-                cx.spawn_in(window, async move |this, cx| {
-                    let serialized_layout =
-                        persistence::get_serialized_pane_layout(adapter_name).await;
+            match task.await {
+                Err(e) => {
+                    this.update(cx, |this, cx| {
+                        this.workspace
+                            .update(cx, |workspace, cx| {
+                                workspace.show_error(&e, cx);
+                            })
+                            .ok();
+                    })
+                    .ok();
 
-                    this.update_in(cx, |this, window, cx| {
-                        let Some(project) = this.project.upgrade() else {
-                            return log::error!(
-                                "Debug Panel out lived it's weak reference to Project"
-                            );
-                        };
+                    session
+                        .update(cx, |session, cx| session.shutdown(cx))?
+                        .await;
+                }
+                Ok(_) => Self::register_session(this, session, cx).await?,
+            }
 
-                        if this
-                            .sessions
-                            .iter()
-                            .any(|item| item.read(cx).session_id(cx) == session_id)
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    async fn register_session(
+        this: WeakEntity<Self>,
+        session: Entity<Session>,
+        cx: &mut AsyncWindowContext,
+    ) -> Result<()> {
+        let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
+        this.update_in(cx, |_, window, cx| {
+            cx.subscribe_in(
+                &session,
+                window,
+                move |_, session, event: &SessionStateEvent, window, cx| match event {
+                    SessionStateEvent::Restart => {
+                        let mut curr_session = session.clone();
+                        while let Some(parent_session) = curr_session
+                            .read_with(cx, |session, _| session.parent_session().cloned())
                         {
-                            // We already have an item for this session.
-                            debug_panic!("We should never reuse session ids");
-                            return;
+                            curr_session = parent_session;
                         }
 
-                        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(),
-                            session,
-                            cx.weak_entity(),
-                            serialized_layout,
-                            window,
-                            cx,
-                        );
+                        let definition = curr_session.update(cx, |session, _| session.definition());
+                        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
 
-                        if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
-                            // We might want to make this an event subscription and only notify when a new thread is selected
-                            // This is used to filter the command menu correctly
-                            cx.observe(&running, |_, _, cx| cx.notify()).detach();
-                        }
+                        let definition = definition.clone();
+                        cx.spawn_in(window, async move |this, cx| {
+                            task.await;
+
+                            this.update_in(cx, |this, window, cx| {
+                                this.start_session(definition, window, cx)
+                            })
+                        })
+                        .detach_and_log_err(cx);
+                    }
+                    _ => {}
+                },
+            )
+            .detach();
+        })
+        .ok();
+
+        let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
+
+        let workspace = this.update_in(cx, |this, window, cx| {
+            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(
+                this.project.clone(),
+                this.workspace.clone(),
+                session,
+                cx.weak_entity(),
+                serialized_layout,
+                window,
+                cx,
+            );
+
+            if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
+                // We might want to make this an event subscription and only notify when a new thread is selected
+                // This is used to filter the command menu correctly
+                cx.observe(&running, |_, _, cx| cx.notify()).detach();
+            }
+
+            this.sessions.push(session_item.clone());
+            this.activate_session(session_item, window, cx);
+            this.workspace.clone()
+        })?;
+
+        workspace.update_in(cx, |workspace, window, cx| {
+            workspace.focus_panel::<Self>(window, cx);
+        })?;
+        Ok(())
+    }
+
+    pub fn start_child_session(
+        &mut self,
+        request: &StartDebuggingRequestArguments,
+        parent_session: Entity<Session>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(worktree) = parent_session.read(cx).worktree() else {
+            log::error!("Attempted to start a child session from non local debug session");
+            return;
+        };
+
+        let dap_store_handle = self.project.read(cx).dap_store().clone();
+        let breakpoint_store = self.project.read(cx).breakpoint_store();
+        let definition = parent_session.read(cx).definition().clone();
+        let mut binary = parent_session.read(cx).binary().clone();
+        binary.request_args = request.clone();
 
-                        this.sessions.push(session_item.clone());
-                        this.activate_session(session_item, window, cx);
+        cx.spawn_in(window, async move |this, cx| {
+            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
+                let session =
+                    dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
+
+                let task = session.update(cx, |session, cx| {
+                    session.boot(
+                        binary,
+                        worktree,
+                        breakpoint_store,
+                        dap_store_handle.downgrade(),
+                        cx,
+                    )
+                });
+                (session, task)
+            })?;
+
+            match task.await {
+                Err(e) => {
+                    this.update(cx, |this, cx| {
+                        this.workspace
+                            .update(cx, |workspace, cx| {
+                                workspace.show_error(&e, cx);
+                            })
+                            .ok();
                     })
-                })
-                .detach();
+                    .ok();
+
+                    session
+                        .update(cx, |session, cx| session.shutdown(cx))?
+                        .await;
+                }
+                Ok(_) => Self::register_session(this, session, cx).await?,
             }
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
+        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 {
                 title,
                 cwd,
@@ -374,6 +487,12 @@ impl DebugPanel {
                 )
                 .detach_and_log_err(cx);
             }
+            dap_store::DapStoreEvent::SpawnChildSession {
+                request,
+                parent_session,
+            } => {
+                self.start_child_session(request, parent_session.clone(), window, cx);
+            }
             _ => {}
         }
     }
@@ -408,7 +527,7 @@ impl DebugPanel {
                         cwd,
                         title,
                     },
-                    task::RevealStrategy::Always,
+                    task::RevealStrategy::Never,
                     window,
                     cx,
                 );
@@ -468,8 +587,6 @@ impl DebugPanel {
                 let session = this.dap_store().read(cx).session_by_id(session_id);
                 session.map(|session| !session.read(cx).is_terminated())
             })
-            .ok()
-            .flatten()
             .unwrap_or_default();
 
         cx.spawn_in(window, async move |this, cx| {
@@ -893,7 +1010,6 @@ impl DebugPanel {
 
 impl EventEmitter<PanelEvent> for DebugPanel {}
 impl EventEmitter<DebugPanelEvent> for DebugPanel {}
-impl EventEmitter<project::Event> for DebugPanel {}
 
 impl Focusable for DebugPanel {
     fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -1039,3 +1155,15 @@ impl Render for DebugPanel {
             .into_any()
     }
 }
+
+struct DebuggerProvider(Entity<DebugPanel>);
+
+impl workspace::DebuggerProvider for DebuggerProvider {
+    fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
+        self.0.update(cx, |_, cx| {
+            cx.defer_in(window, |this, window, cx| {
+                this.start_session(definition, window, cx);
+            })
+        })
+    }
+}

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -16,7 +16,7 @@ mod new_session_modal;
 mod persistence;
 pub(crate) mod session;
 
-#[cfg(test)]
+#[cfg(any(test, feature = "test-support"))]
 pub mod tests;
 
 actions!(

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -4,14 +4,12 @@ use std::{
     path::{Path, PathBuf},
 };
 
-use anyhow::{Result, anyhow};
 use dap::{DapRegistry, DebugRequest};
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{
     App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
     WeakEntity,
 };
-use project::Project;
 use settings::Settings;
 use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
 use theme::ThemeSettings;
@@ -21,7 +19,6 @@ use ui::{
     LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
     ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
 };
-use util::ResultExt;
 use workspace::{ModalView, Workspace};
 
 use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -88,11 +85,11 @@ impl NewSessionModal {
         }
     }
 
-    fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
+    fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
         let request = self.mode.debug_task(cx);
-        Some(DebugTaskDefinition {
-            adapter: self.debugger.clone()?.to_string(),
-            label: suggested_label(&request, self.debugger.as_deref()?),
+        DebugTaskDefinition {
+            adapter: debugger.to_owned(),
+            label: suggested_label(&request, debugger),
             request,
             initialize_args: self.initialize_args.clone(),
             tcp_connection: None,
@@ -100,26 +97,26 @@ impl NewSessionModal {
                 ToggleState::Selected => Some(true),
                 _ => None,
             },
-        })
+        }
     }
 
-    fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
-        let workspace = self.workspace.clone();
-        let config = self
-            .debug_config(cx)
-            .ok_or_else(|| anyhow!("Failed to create a debug config"))?;
-
-        let _ = self.debug_panel.update(cx, |panel, _| {
-            panel.past_debug_definition = Some(config.clone());
-        });
+    fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(debugger) = self.debugger.as_ref() else {
+            // todo: show in UI.
+            log::error!("No debugger selected");
+            return;
+        };
+        let config = self.debug_config(cx, debugger);
+        let debug_panel = self.debug_panel.clone();
 
-        let task_contexts = workspace
+        let task_contexts = self
+            .workspace
             .update(cx, |workspace, cx| {
                 tasks_ui::task_contexts(workspace, window, cx)
             })
             .ok();
 
-        cx.spawn(async move |this, cx| {
+        cx.spawn_in(window, async move |this, cx| {
             let task_context = if let Some(task) = task_contexts {
                 task.await
                     .active_worktree_context
@@ -127,9 +124,8 @@ impl NewSessionModal {
             } else {
                 task::TaskContext::default()
             };
-            let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
 
-            let task = project.update(cx, |this, cx| {
+            debug_panel.update_in(cx, |debug_panel, window, cx| {
                 let template = DebugTaskTemplate {
                     locator: None,
                     definition: config.clone(),
@@ -139,23 +135,18 @@ impl NewSessionModal {
                     .resolve_task("debug_task", &task_context)
                     .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
                 {
-                    this.start_debug_session(debug_config.definition, cx)
+                    debug_panel.start_session(debug_config.definition, window, cx)
                 } else {
-                    this.start_debug_session(config, cx)
+                    debug_panel.start_session(config, window, cx)
                 }
             })?;
-            let spawn_result = task.await;
-            if spawn_result.is_ok() {
-                this.update(cx, |_, cx| {
-                    cx.emit(DismissEvent);
-                })
-                .ok();
-            }
-            spawn_result?;
+            this.update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
             anyhow::Result::<_, anyhow::Error>::Ok(())
         })
         .detach_and_log_err(cx);
-        Ok(())
     }
 
     fn update_attach_picker(
@@ -249,15 +240,12 @@ impl NewSessionModal {
                                     );
                                 }
                                 DebugRequest::Attach(_) => {
-                                    let Ok(project) = this
-                                        .workspace
-                                        .read_with(cx, |this, _| this.project().clone())
-                                    else {
+                                    let Some(workspace) = this.workspace.upgrade() else {
                                         return;
                                     };
                                     this.mode = NewSessionMode::attach(
                                         this.debugger.clone(),
-                                        project,
+                                        workspace,
                                         window,
                                         cx,
                                     );
@@ -357,7 +345,7 @@ struct AttachMode {
 impl AttachMode {
     fn new(
         debugger: Option<SharedString>,
-        project: Entity<Project>,
+        workspace: Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<NewSessionModal>,
     ) -> Entity<Self> {
@@ -370,7 +358,7 @@ impl AttachMode {
             stop_on_entry: Some(false),
         };
         let attach_picker = cx.new(|cx| {
-            let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
+            let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
             window.focus(&modal.focus_handle(cx));
 
             modal
@@ -470,11 +458,11 @@ impl RenderOnce for NewSessionMode {
 impl NewSessionMode {
     fn attach(
         debugger: Option<SharedString>,
-        project: Entity<Project>,
+        workspace: Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<NewSessionModal>,
     ) -> Self {
-        Self::Attach(AttachMode::new(debugger, project, window, cx))
+        Self::Attach(AttachMode::new(debugger, workspace, window, cx))
     }
     fn launch(
         past_launch_config: Option<LaunchRequest>,
@@ -569,15 +557,12 @@ impl Render for NewSessionModal {
                                 .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
                                 .style(ui::ButtonStyle::Subtle)
                                 .on_click(cx.listener(|this, _, window, cx| {
-                                    let Ok(project) = this
-                                        .workspace
-                                        .read_with(cx, |this, _| this.project().clone())
-                                    else {
+                                    let Some(workspace) = this.workspace.upgrade() else {
                                         return;
                                     };
                                     this.mode = NewSessionMode::attach(
                                         this.debugger.clone(),
-                                        project,
+                                        workspace,
                                         window,
                                         cx,
                                     );
@@ -631,7 +616,7 @@ impl Render for NewSessionModal {
                             .child(
                                 Button::new("debugger-spawn", "Start")
                                     .on_click(cx.listener(|this, _, window, cx| {
-                                        this.start_new_session(window, cx).log_err();
+                                        this.start_new_session(window, cx);
                                     }))
                                     .disabled(self.debugger.is_none()),
                             ),

crates/debugger_ui/src/session.rs 🔗

@@ -88,6 +88,12 @@ impl DebugSession {
         }
     }
 
+    pub fn session(&self, cx: &App) -> Entity<Session> {
+        match &self.mode {
+            DebugSessionState::Running(entity) => entity.read(cx).session().clone(),
+        }
+    }
+
     pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
         match &self.mode {
             DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
@@ -115,13 +121,7 @@ impl DebugSession {
         };
 
         self.label
-            .get_or_init(|| {
-                session
-                    .read(cx)
-                    .as_local()
-                    .expect("Remote Debug Sessions are not implemented yet")
-                    .label()
-            })
+            .get_or_init(|| session.read(cx).label())
             .to_owned()
     }
 

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

@@ -418,6 +418,19 @@ impl RunningState {
                         let threads = this.session.update(cx, |this, cx| this.threads(cx));
                         this.select_current_thread(&threads, cx);
                     }
+                    SessionEvent::CapabilitiesLoaded => {
+                        let capabilities = this.capabilities(cx);
+                        if !capabilities.supports_modules_request.unwrap_or(false) {
+                            this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
+                        }
+                        if !capabilities
+                            .supports_loaded_sources_request
+                            .unwrap_or(false)
+                        {
+                            this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
+                        }
+                    }
+
                     _ => {}
                 }
                 cx.notify()
@@ -447,35 +460,14 @@ impl RunningState {
             workspace::PaneGroup::with_root(root)
         } else {
             pane_close_subscriptions.clear();
-            let module_list = if session
-                .read(cx)
-                .capabilities()
-                .supports_modules_request
-                .unwrap_or(false)
-            {
-                Some(&module_list)
-            } else {
-                None
-            };
-
-            let loaded_source_list = if session
-                .read(cx)
-                .capabilities()
-                .supports_loaded_sources_request
-                .unwrap_or(false)
-            {
-                Some(&loaded_source_list)
-            } else {
-                None
-            };
 
             let root = Self::default_pane_layout(
                 project,
                 &workspace,
                 &stack_frame_list,
                 &variable_list,
-                module_list,
-                loaded_source_list,
+                &module_list,
+                &loaded_source_list,
                 &console,
                 &breakpoint_list,
                 &mut pane_close_subscriptions,
@@ -512,11 +504,6 @@ impl RunningState {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        debug_assert!(
-            item_kind.is_supported(self.session.read(cx).capabilities()),
-            "We should only allow removing supported item kinds"
-        );
-
         if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
             Some(pane).zip(
                 pane.read(cx)
@@ -946,8 +933,8 @@ impl RunningState {
         workspace: &WeakEntity<Workspace>,
         stack_frame_list: &Entity<StackFrameList>,
         variable_list: &Entity<VariableList>,
-        module_list: Option<&Entity<ModuleList>>,
-        loaded_source_list: Option<&Entity<LoadedSourceList>>,
+        module_list: &Entity<ModuleList>,
+        loaded_source_list: &Entity<LoadedSourceList>,
         console: &Entity<Console>,
         breakpoints: &Entity<BreakpointList>,
         subscriptions: &mut HashMap<EntityId, Subscription>,
@@ -1003,41 +990,36 @@ impl RunningState {
                 window,
                 cx,
             );
-            if let Some(module_list) = module_list {
-                this.add_item(
-                    Box::new(SubView::new(
-                        module_list.focus_handle(cx),
-                        module_list.clone().into(),
-                        DebuggerPaneItem::Modules,
-                        None,
-                        cx,
-                    )),
-                    false,
-                    false,
+            this.add_item(
+                Box::new(SubView::new(
+                    module_list.focus_handle(cx),
+                    module_list.clone().into(),
+                    DebuggerPaneItem::Modules,
                     None,
-                    window,
                     cx,
-                );
-                this.activate_item(0, false, false, window, cx);
-            }
+                )),
+                false,
+                false,
+                None,
+                window,
+                cx,
+            );
 
-            if let Some(loaded_source_list) = loaded_source_list {
-                this.add_item(
-                    Box::new(SubView::new(
-                        loaded_source_list.focus_handle(cx),
-                        loaded_source_list.clone().into(),
-                        DebuggerPaneItem::LoadedSources,
-                        None,
-                        cx,
-                    )),
-                    false,
-                    false,
+            this.add_item(
+                Box::new(SubView::new(
+                    loaded_source_list.focus_handle(cx),
+                    loaded_source_list.clone().into(),
+                    DebuggerPaneItem::LoadedSources,
                     None,
-                    window,
                     cx,
-                );
-                this.activate_item(1, false, false, window, cx);
-            }
+                )),
+                false,
+                false,
+                None,
+                window,
+                cx,
+            );
+            this.activate_item(0, false, false, window, cx);
         });
 
         let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);

crates/debugger_ui/src/tests.rs 🔗

@@ -1,16 +1,29 @@
+use std::sync::Arc;
+
+use anyhow::{Result, anyhow};
+use dap::{DebugRequest, client::DebugAdapterClient};
 use gpui::{Entity, TestAppContext, WindowHandle};
-use project::Project;
+use project::{Project, debugger::session::Session};
 use settings::SettingsStore;
+use task::DebugTaskDefinition;
 use terminal_view::terminal_panel::TerminalPanel;
 use workspace::Workspace;
 
 use crate::{debugger_panel::DebugPanel, session::DebugSession};
 
+#[cfg(test)]
 mod attach_modal;
+#[cfg(test)]
 mod console;
+#[cfg(test)]
+mod dap_logger;
+#[cfg(test)]
 mod debugger_panel;
+#[cfg(test)]
 mod module_list;
+#[cfg(test)]
 mod stack_frame_list;
+#[cfg(test)]
 mod variable_list;
 
 pub fn init_test(cx: &mut gpui::TestAppContext) {
@@ -42,7 +55,7 @@ pub async fn init_test_workspace(
     let debugger_panel = workspace_handle
         .update(cx, |_, window, cx| {
             cx.spawn_in(window, async move |this, cx| {
-                DebugPanel::load(this, cx.clone()).await
+                DebugPanel::load(this, cx).await
             })
         })
         .unwrap()
@@ -82,3 +95,46 @@ pub fn active_debug_session_panel(
         })
         .unwrap()
 }
+
+pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
+    workspace: &WindowHandle<Workspace>,
+    cx: &mut gpui::TestAppContext,
+    config: DebugTaskDefinition,
+    configure: T,
+) -> Result<Entity<Session>> {
+    let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
+    workspace.update(cx, |workspace, window, cx| {
+        workspace.start_debug_session(config, window, cx)
+    })?;
+    cx.run_until_parked();
+    let session = workspace.read_with(cx, |workspace, cx| {
+        workspace
+            .panel::<DebugPanel>(cx)
+            .and_then(|panel| panel.read(cx).active_session())
+            .and_then(|session| session.read(cx).mode().as_running().cloned())
+            .map(|running| running.read(cx).session().clone())
+            .ok_or_else(|| anyhow!("Failed to get active session"))
+    })??;
+
+    Ok(session)
+}
+
+pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
+    workspace: &WindowHandle<Workspace>,
+    cx: &mut gpui::TestAppContext,
+    configure: T,
+) -> Result<Entity<Session>> {
+    start_debug_session_with(
+        workspace,
+        cx,
+        DebugTaskDefinition {
+            adapter: "fake-adapter".to_string(),
+            request: DebugRequest::Launch(Default::default()),
+            label: "test".to_string(),
+            initialize_args: None,
+            tcp_connection: None,
+            stop_on_entry: None,
+        },
+        configure,
+    )
+}

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

@@ -1,4 +1,4 @@
-use crate::{attach_modal::Candidate, *};
+use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
 use attach_modal::AttachModal;
 use dap::{FakeAdapter, client::SessionId};
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
@@ -26,8 +26,8 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session_with(
-        &project,
+    let session = start_debug_session_with(
+        &workspace,
         cx,
         DebugTaskDefinition {
             adapter: "fake-adapter".to_string(),
@@ -47,7 +47,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
             });
         },
     )
-    .await
     .unwrap();
 
     cx.run_until_parked();
@@ -99,9 +98,10 @@ async fn test_show_attach_modal_and_select_process(
         });
     let attach_modal = workspace
         .update(cx, |workspace, window, cx| {
+            let workspace_handle = cx.entity();
             workspace.toggle_modal(window, cx, |window, cx| {
                 AttachModal::with_processes(
-                    project.clone(),
+                    workspace_handle,
                     DebugTaskDefinition {
                         adapter: FakeAdapter::ADAPTER_NAME.into(),
                         request: dap::DebugRequest::Attach(AttachRequest::default()),

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

@@ -1,4 +1,7 @@
-use crate::{tests::active_debug_session_panel, *};
+use crate::{
+    tests::{active_debug_session_panel, start_debug_session},
+    *,
+};
 use dap::requests::StackTrace;
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use project::{FakeFs, Project};
@@ -28,9 +31,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
         })
         .unwrap();
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<StackTrace, _>(move |_, _| {

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

@@ -0,0 +1,118 @@
+use crate::tests::{init_test, init_test_workspace, start_debug_session};
+use dap::requests::{StackTrace, Threads};
+use debugger_tools::LogStore;
+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
+use project::Project;
+use serde_json::json;
+use std::cell::OnceCell;
+
+#[gpui::test]
+async fn test_dap_logger_captures_all_session_rpc_messages(
+    executor: BackgroundExecutor,
+    cx: &mut TestAppContext,
+) {
+    let log_store_cell = std::rc::Rc::new(OnceCell::new());
+
+    cx.update(|cx| {
+        let log_store_cell = log_store_cell.clone();
+        cx.observe_new::<LogStore>(move |_, _, cx| {
+            log_store_cell.set(cx.entity()).unwrap();
+        })
+        .detach();
+        debugger_tools::init(cx);
+    });
+    init_test(cx);
+
+    let log_store = log_store_cell.get().unwrap().clone();
+
+    // Create a filesystem with a simple project
+    let fs = project::FakeFs::new(executor.clone());
+    fs.insert_tree(
+        "/project",
+        json!({
+            "main.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"
+        }),
+    )
+    .await;
+
+    assert!(
+        log_store.read_with(cx, |log_store, _| log_store
+            .contained_session_ids()
+            .is_empty()),
+        "log_store shouldn't contain any session IDs before any sessions were created"
+    );
+
+    let project = Project::test(fs, ["/project".as_ref()], cx).await;
+
+    let workspace = init_test_workspace(&project, cx).await;
+    let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+    // Start a debug session
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+    let session_id = session.read_with(cx, |session, _| session.session_id());
+    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+    assert_eq!(
+        log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
+        1,
+    );
+
+    assert!(
+        log_store.read_with(cx, |log_store, _| log_store
+            .contained_session_ids()
+            .contains(&session_id)),
+        "log_store should contain the session IDs of the started session"
+    );
+
+    assert!(
+        !log_store.read_with(cx, |log_store, _| log_store
+            .rpc_messages_for_session_id(session_id)
+            .is_empty()),
+        "We should have the initialization sequence in the log store"
+    );
+
+    // Set up basic responses for common requests
+    client.on_request::<Threads, _>(move |_, _| {
+        Ok(dap::ThreadsResponse {
+            threads: vec![dap::Thread {
+                id: 1,
+                name: "Thread 1".into(),
+            }],
+        })
+    });
+
+    client.on_request::<StackTrace, _>(move |_, _| {
+        Ok(dap::StackTraceResponse {
+            stack_frames: Vec::default(),
+            total_frames: None,
+        })
+    });
+
+    // Run until all pending tasks are executed
+    cx.run_until_parked();
+
+    // Simulate a stopped event to generate more DAP messages
+    client
+        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+            reason: dap::StoppedEventReason::Pause,
+            description: None,
+            thread_id: Some(1),
+            preserve_focus_hint: None,
+            text: None,
+            all_threads_stopped: None,
+            hit_breakpoint_ids: None,
+        }))
+        .await;
+
+    cx.run_until_parked();
+
+    // Shutdown the debug session
+    let shutdown_session = project.update(cx, |project, cx| {
+        project.dap_store().update(cx, |dap_store, cx| {
+            dap_store.shutdown_session(session.read(cx).session_id(), cx)
+        })
+    });
+
+    shutdown_session.await.unwrap();
+    cx.run_until_parked();
+}

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

@@ -1,4 +1,4 @@
-use crate::*;
+use crate::{tests::start_debug_session, *};
 use dap::{
     ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
     StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
@@ -48,9 +48,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<Threads, _>(move |_, _| {
@@ -187,9 +185,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<Threads, _>(move |_, _| {
@@ -354,9 +350,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client
@@ -419,6 +413,86 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
     shutdown_session.await.unwrap();
 }
 
+#[gpui::test]
+async fn test_handle_start_debugging_request(
+    executor: BackgroundExecutor,
+    cx: &mut TestAppContext,
+) {
+    init_test(cx);
+
+    let fs = FakeFs::new(executor.clone());
+
+    fs.insert_tree(
+        "/project",
+        json!({
+            "main.rs": "First line\nSecond line\nThird line\nFourth line",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/project".as_ref()], cx).await;
+    let workspace = init_test_workspace(&project, cx).await;
+    let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+    let fake_config = json!({"one": "two"});
+    let launched_with = Arc::new(parking_lot::Mutex::new(None));
+
+    let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
+        let launched_with = launched_with.clone();
+        move |client| {
+            let launched_with = launched_with.clone();
+            client.on_request::<dap::requests::Launch, _>(move |_, args| {
+                launched_with.lock().replace(args.raw);
+                Ok(())
+            });
+            client.on_request::<dap::requests::Attach, _>(move |_, _| {
+                assert!(false, "should not get attach request");
+                Ok(())
+            });
+        }
+    });
+
+    client
+        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
+            request: StartDebuggingRequestArgumentsRequest::Launch,
+            configuration: fake_config.clone(),
+        })
+        .await;
+
+    cx.run_until_parked();
+
+    workspace
+        .update(cx, |workspace, _window, cx| {
+            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
+            let active_session = debug_panel
+                .read(cx)
+                .active_session()
+                .unwrap()
+                .read(cx)
+                .session(cx);
+            let parent_session = active_session.read(cx).parent_session().unwrap();
+
+            assert_eq!(
+                active_session.read(cx).definition(),
+                parent_session.read(cx).definition()
+            );
+        })
+        .unwrap();
+
+    assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
+
+    let shutdown_session = project.update(cx, |project, cx| {
+        project.dap_store().update(cx, |dap_store, cx| {
+            dap_store.shutdown_session(session.read(cx).session_id(), cx)
+        })
+    });
+
+    shutdown_session.await.unwrap();
+}
+
 // // covers that we always send a response back, if something when wrong,
 // // while spawning the terminal
 #[gpui::test]
@@ -444,9 +518,7 @@ async fn test_handle_error_run_in_terminal_reverse_request(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client
@@ -522,9 +594,7 @@ async fn test_handle_start_debugging_reverse_request(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -629,9 +699,7 @@ async fn test_shutdown_children_when_parent_session_shutdown(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -737,9 +805,7 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_response::<StartDebugging, _>(move |_| {}).await;
@@ -858,7 +924,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |client| {
+    let session = start_debug_session(&workspace, cx, |client| {
         client.on_request::<dap::requests::Initialize, _>(move |_, _| {
             Ok(dap::Capabilities {
                 supports_step_back: Some(true),
@@ -866,7 +932,6 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
             })
         });
     })
-    .await
     .unwrap();
 
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
@@ -1073,9 +1138,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
         .update(cx, |_, _, cx| worktree.read(cx).id())
         .unwrap();
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     let buffer = project
@@ -1290,9 +1353,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
         editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
     });
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     let called_set_breakpoints = Arc::new(AtomicBool::new(false));
@@ -1358,7 +1419,7 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let task = project::debugger::test::start_debug_session(&project, cx, |client| {
+    start_debug_session(&workspace, cx, |client| {
         client.on_request::<dap::requests::Initialize, _>(|_, _| {
             Err(ErrorResponse {
                 error: Some(Message {
@@ -1372,12 +1433,8 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
                 }),
             })
         });
-    });
-
-    assert!(
-        task.await.is_err(),
-        "Session should failed to start if launch request fails"
-    );
+    })
+    .ok();
 
     cx.run_until_parked();
 

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

@@ -1,16 +1,13 @@
 use crate::{
     debugger_panel::DebugPanel,
-    tests::{active_debug_session_panel, init_test, init_test_workspace},
+    tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
 };
 use dap::{
     StoppedEvent,
     requests::{Initialize, Modules},
 };
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{
-    FakeFs, Project,
-    debugger::{self},
-};
+use project::{FakeFs, Project};
 use std::sync::{
     Arc,
     atomic::{AtomicBool, AtomicI32, Ordering},
@@ -31,7 +28,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
         .unwrap();
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |client| {
+    let session = start_debug_session(&workspace, cx, |client| {
         client.on_request::<Initialize, _>(move |_, _| {
             Ok(dap::Capabilities {
                 supports_modules_request: Some(true),
@@ -39,7 +36,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
             })
         });
     })
-    .await
     .unwrap();
 
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());

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

@@ -1,7 +1,7 @@
 use crate::{
     debugger_panel::DebugPanel,
     session::running::stack_frame_list::StackFrameEntry,
-    tests::{active_debug_session_panel, init_test, init_test_workspace},
+    tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
 };
 use dap::{
     StackFrame,
@@ -9,7 +9,7 @@ use dap::{
 };
 use editor::{Editor, ToPoint as _};
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{FakeFs, Project, debugger};
+use project::{FakeFs, Project};
 use serde_json::json;
 use std::sync::Arc;
 use unindent::Unindent as _;
@@ -50,9 +50,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
     let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
     client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
 
@@ -229,9 +227,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
     });
 
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<Threads, _>(move |_, _| {
@@ -495,9 +491,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<Threads, _>(move |_, _| {

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

@@ -6,7 +6,7 @@ use std::sync::{
 use crate::{
     DebugPanel,
     session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
-    tests::{active_debug_session_panel, init_test, init_test_workspace},
+    tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
 };
 use collections::HashMap;
 use dap::{
@@ -15,7 +15,7 @@ use dap::{
 };
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use menu::{SelectFirst, SelectNext, SelectPrevious};
-use project::{FakeFs, Project, debugger};
+use project::{FakeFs, Project};
 use serde_json::json;
 use unindent::Unindent as _;
 use util::path;
@@ -54,9 +54,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
         })
         .unwrap();
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -266,9 +264,7 @@ async fn test_fetch_variables_for_multiple_scopes(
         .unwrap();
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -528,9 +524,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
         })
         .unwrap();
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1313,9 +1307,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1560,9 +1552,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
         .unwrap();
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
-    let session = debugger::test::start_debug_session(&project, cx, |_| {})
-        .await
-        .unwrap();
+    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
     let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 
     client.on_request::<dap::requests::Threads, _>(move |_, _| {

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

@@ -4,33 +4,35 @@ use super::{
     session::{self, Session, SessionStateEvent},
 };
 use crate::{
-    ProjectEnvironment, debugger, project_settings::ProjectSettings, worktree_store::WorktreeStore,
+    ProjectEnvironment,
+    project_settings::ProjectSettings,
+    terminals::{SshCommand, wrap_for_ssh},
+    worktree_store::WorktreeStore,
 };
 use anyhow::{Result, anyhow};
 use async_trait::async_trait;
 use collections::HashMap;
 use dap::{
-    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, ErrorResponse,
-    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
-    Source, StartDebuggingRequestArguments,
-    adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName},
+    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments,
+    EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source,
+    StartDebuggingRequestArguments,
+    adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments},
     client::SessionId,
     messages::Message,
     requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
 };
 use fs::Fs;
 use futures::{
-    channel::{mpsc, oneshot},
+    channel::mpsc,
     future::{Shared, join_all},
 };
-use gpui::{
-    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
-};
+use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 use http_client::HttpClient;
 use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore};
 use lsp::LanguageServerName;
 use node_runtime::NodeRuntime;
 
+use remote::SshRemoteClient;
 use rpc::{
     AnyProtoClient, TypedEnvelope,
     proto::{self},
@@ -42,6 +44,7 @@ use std::{
     borrow::Borrow,
     collections::{BTreeMap, HashSet},
     ffi::OsStr,
+    net::Ipv4Addr,
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -66,6 +69,10 @@ pub enum DapStoreEvent {
         envs: HashMap<String, String>,
         sender: mpsc::Sender<Result<u32>>,
     },
+    SpawnChildSession {
+        request: StartDebuggingRequestArguments,
+        parent_session: Entity<Session>,
+    },
     Notification(String),
     RemoteHasInitialized,
 }
@@ -83,12 +90,12 @@ pub struct LocalDapStore {
     http_client: Arc<dyn HttpClient>,
     environment: Entity<ProjectEnvironment>,
     language_registry: Arc<LanguageRegistry>,
-    worktree_store: Entity<WorktreeStore>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
     locators: HashMap<String, Arc<dyn DapLocator>>,
 }
 
 pub struct SshDapStore {
+    ssh_client: Entity<SshRemoteClient>,
     upstream_client: AnyProtoClient,
     upstream_project_id: u64,
 }
@@ -97,6 +104,7 @@ pub struct DapStore {
     mode: DapStoreMode,
     downstream_client: Option<(AnyProtoClient, u64)>,
     breakpoint_store: Entity<BreakpointStore>,
+    worktree_store: Entity<WorktreeStore>,
     sessions: BTreeMap<SessionId, Entity<Session>>,
     next_session_id: u32,
     start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
@@ -136,40 +144,43 @@ impl DapStore {
             http_client,
             node_runtime,
             toolchain_store,
-            worktree_store,
             language_registry,
             locators,
         });
 
-        Self::new(mode, breakpoint_store, cx)
+        Self::new(mode, breakpoint_store, worktree_store, cx)
     }
 
     pub fn new_ssh(
         project_id: u64,
-        upstream_client: AnyProtoClient,
+        ssh_client: Entity<SshRemoteClient>,
         breakpoint_store: Entity<BreakpointStore>,
+        worktree_store: Entity<WorktreeStore>,
         cx: &mut Context<Self>,
     ) -> Self {
         let mode = DapStoreMode::Ssh(SshDapStore {
-            upstream_client,
+            upstream_client: ssh_client.read(cx).proto_client(),
+            ssh_client,
             upstream_project_id: project_id,
         });
 
-        Self::new(mode, breakpoint_store, cx)
+        Self::new(mode, breakpoint_store, worktree_store, cx)
     }
 
     pub fn new_collab(
         _project_id: u64,
         _upstream_client: AnyProtoClient,
         breakpoint_store: Entity<BreakpointStore>,
+        worktree_store: Entity<WorktreeStore>,
         cx: &mut Context<Self>,
     ) -> Self {
-        Self::new(DapStoreMode::Collab, breakpoint_store, cx)
+        Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
     }
 
     fn new(
         mode: DapStoreMode,
         breakpoint_store: Entity<BreakpointStore>,
+        worktree_store: Entity<WorktreeStore>,
         cx: &mut Context<Self>,
     ) -> Self {
         let (start_debugging_tx, mut message_rx) =
@@ -202,6 +213,7 @@ impl DapStore {
             next_session_id: 0,
             downstream_client: None,
             breakpoint_store,
+            worktree_store,
             sessions: Default::default(),
         }
     }
@@ -212,8 +224,8 @@ impl DapStore {
         cx: &mut Context<Self>,
     ) -> Task<Result<DebugAdapterBinary>> {
         match &self.mode {
-            DapStoreMode::Local(local) => {
-                let Some(worktree) = local.worktree_store.read(cx).visible_worktrees(cx).next()
+            DapStoreMode::Local(_) => {
+                let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
                 else {
                     return Task::ready(Err(anyhow!("Failed to find a worktree")));
                 };
@@ -261,10 +273,49 @@ impl DapStore {
                     project_id: ssh.upstream_project_id,
                     task: Some(definition.to_proto()),
                 });
+                let ssh_client = ssh.ssh_client.clone();
 
-                cx.background_spawn(async move {
+                cx.spawn(async move |_, cx| {
                     let response = request.await?;
-                    DebugAdapterBinary::from_proto(response)
+                    let binary = DebugAdapterBinary::from_proto(response)?;
+                    let mut ssh_command = ssh_client.update(cx, |ssh, _| {
+                        anyhow::Ok(SshCommand {
+                            arguments: ssh
+                                .ssh_args()
+                                .ok_or_else(|| anyhow!("SSH arguments not found"))?,
+                        })
+                    })??;
+
+                    let mut connection = None;
+                    if let Some(c) = binary.connection {
+                        let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
+                        let port =
+                            dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
+
+                        ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
+                        connection = Some(TcpArguments {
+                            port: c.port,
+                            host: local_bind_addr,
+                            timeout: c.timeout,
+                        })
+                    }
+
+                    let (program, args) = wrap_for_ssh(
+                        &ssh_command,
+                        Some((&binary.command, &binary.arguments)),
+                        binary.cwd.as_deref(),
+                        binary.envs,
+                        None,
+                    );
+
+                    Ok(DebugAdapterBinary {
+                        command: program,
+                        arguments: args,
+                        envs: HashMap::default(),
+                        cwd: None,
+                        connection,
+                        request_args: binary.request_args,
+                    })
                 })
             }
             DapStoreMode::Collab => {
@@ -316,27 +367,79 @@ impl DapStore {
         }
     }
 
-    pub fn add_remote_client(
+    pub fn new_session(
         &mut self,
-        session_id: SessionId,
-        ignore: Option<bool>,
+        template: DebugTaskDefinition,
+        parent_session: Option<Entity<Session>>,
         cx: &mut Context<Self>,
-    ) {
-        if let DapStoreMode::Ssh(remote) = &self.mode {
-            self.sessions.insert(
-                session_id,
-                cx.new(|_| {
-                    debugger::session::Session::remote(
-                        session_id,
-                        remote.upstream_client.clone(),
-                        remote.upstream_project_id,
-                        ignore.unwrap_or(false),
-                    )
-                }),
-            );
-        } else {
-            debug_assert!(false);
+    ) -> Entity<Session> {
+        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
+
+        if let Some(session) = &parent_session {
+            session.update(cx, |session, _| {
+                session.add_child_session_id(session_id);
+            });
         }
+
+        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,
+        );
+
+        self.sessions.insert(session_id, session.clone());
+        cx.notify();
+
+        cx.subscribe(&session, {
+            move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
+                SessionStateEvent::Shutdown => {
+                    this.shutdown_session(session_id, cx).detach_and_log_err(cx);
+                }
+                SessionStateEvent::Restart => {}
+                SessionStateEvent::Running => {
+                    cx.emit(DapStoreEvent::DebugClientStarted(session_id));
+                }
+            }
+        })
+        .detach();
+
+        session
+    }
+
+    pub fn boot_session(
+        &self,
+        session: Entity<Session>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next() else {
+            return Task::ready(Err(anyhow!("Failed to find a worktree")));
+        };
+
+        let dap_store = cx.weak_entity();
+        let breakpoint_store = self.breakpoint_store.clone();
+        let definition = session.read(cx).definition();
+
+        cx.spawn({
+            let session = session.clone();
+            async move |this, cx| {
+                let binary = this
+                    .update(cx, |this, cx| {
+                        this.get_debug_adapter_binary(definition.clone(), cx)
+                    })?
+                    .await?;
+
+                session
+                    .update(cx, |session, cx| {
+                        session.boot(binary, worktree, breakpoint_store, dap_store, cx)
+                    })?
+                    .await
+            }
+        })
     }
 
     pub fn session_by_id(
@@ -367,6 +470,10 @@ impl DapStore {
         &self.breakpoint_store
     }
 
+    pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
+        &self.worktree_store
+    }
+
     #[allow(dead_code)]
     async fn handle_ignore_breakpoint_state(
         this: Entity<Self>,
@@ -407,52 +514,6 @@ impl DapStore {
         )
     }
 
-    pub fn new_session(
-        &mut self,
-        binary: DebugAdapterBinary,
-        config: DebugTaskDefinition,
-        worktree: WeakEntity<Worktree>,
-        parent_session: Option<Entity<Session>>,
-        cx: &mut Context<Self>,
-    ) -> (SessionId, Task<Result<Entity<Session>>>) {
-        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
-
-        if let Some(session) = &parent_session {
-            session.update(cx, |session, _| {
-                session.add_child_session_id(session_id);
-            });
-        }
-
-        let (initialized_tx, initialized_rx) = oneshot::channel();
-
-        let start_debugging_tx = self.start_debugging_tx.clone();
-
-        let task = cx.spawn(async move |this, cx| {
-            let start_client_task = this.update(cx, |this, cx| {
-                Session::local(
-                    this.breakpoint_store.clone(),
-                    worktree.clone(),
-                    session_id,
-                    parent_session,
-                    binary,
-                    config,
-                    start_debugging_tx.clone(),
-                    initialized_tx,
-                    cx,
-                )
-            })?;
-
-            let ret = this
-                .update(cx, |_, cx| {
-                    create_new_session(session_id, initialized_rx, start_client_task, worktree, cx)
-                })?
-                .await;
-            ret
-        });
-
-        (session_id, task)
-    }
-
     fn handle_start_debugging_request(
         &mut self,
         session_id: SessionId,
@@ -462,56 +523,35 @@ impl DapStore {
         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 Some(worktree) = parent_session
-            .read(cx)
-            .as_local()
-            .map(|local| local.worktree().clone())
-        else {
-            return Task::ready(Err(anyhow!(
-                "Cannot handle start debugging request from remote end"
-            )));
-        };
-
-        let args = serde_json::from_value::<StartDebuggingRequestArguments>(
-            request.arguments.unwrap_or_default(),
-        )
-        .expect("To parse StartDebuggingRequestArguments");
-        let mut binary = parent_session.read(cx).binary().clone();
-        let config = parent_session.read(cx).configuration().unwrap().clone();
-        binary.request_args = args;
+        let launch_request: Option<Result<StartDebuggingRequestArguments, _>> = request
+            .arguments
+            .as_ref()
+            .map(|value| serde_json::from_value(value.clone()));
 
-        let new_session_task = self
-            .new_session(binary, config, worktree, Some(parent_session.clone()), cx)
-            .1;
+        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;
+        }
 
-        let request_seq = request.seq;
         cx.spawn(async move |_, cx| {
-            let (success, body) = match new_session_task.await {
-                Ok(_) => (true, None),
-                Err(error) => (
-                    false,
-                    Some(serde_json::to_value(ErrorResponse {
-                        error: Some(dap::Message {
-                            id: request_seq,
-                            format: error.to_string(),
-                            variables: None,
-                            send_telemetry: None,
-                            show_user: None,
-                            url: None,
-                            url_label: None,
-                        }),
-                    })?),
-                ),
-            };
-
             parent_session
                 .update(cx, |session, cx| {
                     session.respond_to_client(
                         request_seq,
                         success,
                         StartDebugging::COMMAND.to_string(),
-                        body,
+                        None,
                         cx,
                     )
                 })?
@@ -752,7 +792,7 @@ impl DapStore {
 
         let shutdown_parent_task = if let Some(parent_session) = session
             .read(cx)
-            .parent_id()
+            .parent_id(cx)
             .and_then(|session_id| self.session_by_id(session_id))
         {
             let shutdown_id = parent_session.update(cx, |parent_session, _| {
@@ -842,121 +882,6 @@ impl DapStore {
     }
 }
 
-fn create_new_session(
-    session_id: SessionId,
-    initialized_rx: oneshot::Receiver<()>,
-    start_client_task: Task<Result<Entity<Session>, anyhow::Error>>,
-    worktree: WeakEntity<Worktree>,
-    cx: &mut Context<DapStore>,
-) -> Task<Result<Entity<Session>>> {
-    let task = cx.spawn(async move |this, cx| {
-        let session = match start_client_task.await {
-            Ok(session) => session,
-            Err(error) => {
-                this.update(cx, |_, cx| {
-                    cx.emit(DapStoreEvent::Notification(error.to_string()));
-                })
-                .log_err();
-
-                return Err(error);
-            }
-        };
-
-        // we have to insert the session early, so we can handle reverse requests
-        // that need the session to be available
-        this.update(cx, |store, cx| {
-            store.sessions.insert(session_id, session.clone());
-            cx.emit(DapStoreEvent::DebugClientStarted(session_id));
-            cx.notify();
-        })?;
-        let seq_result = async || {
-            session
-                .update(cx, |session, cx| session.request_initialize(cx))?
-                .await?;
-
-            session
-                .update(cx, |session, cx| {
-                    session.initialize_sequence(initialized_rx, this.clone(), cx)
-                })?
-                .await
-        };
-        match seq_result().await {
-            Ok(_) => {}
-            Err(error) => {
-                this.update(cx, |this, cx| {
-                    cx.emit(DapStoreEvent::Notification(error.to_string()));
-                    this.shutdown_session(session_id, cx)
-                })?
-                .await
-                .log_err();
-
-                return Err(error);
-            }
-        }
-
-        this.update(cx, |_, cx| {
-            cx.subscribe(
-                &session,
-                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 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 Some((config, binary)) = curr_session.read_with(cx, |session, _| {
-                            session
-                                .configuration()
-                                .map(|config| (config, session.root_binary().clone()))
-                        }) else {
-                            log::error!("Failed to get debug config from session");
-                            return;
-                        };
-
-                        let session_id = curr_session.read(cx).session_id();
-
-                        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
-
-                        let worktree = worktree.clone();
-                        cx.spawn(async move |this, cx| {
-                            task.await;
-
-                            this.update(cx, |this, cx| {
-                                this.sessions.remove(&session_id);
-                                this.new_session(
-                                    binary.as_ref().clone(),
-                                    config,
-                                    worktree,
-                                    None,
-                                    cx,
-                                )
-                            })?
-                            .1
-                            .await?;
-
-                            anyhow::Ok(())
-                        })
-                        .detach_and_log_err(cx);
-                    }
-                },
-            )
-            .detach();
-            cx.emit(DapStoreEvent::DebugSessionInitialized(session_id));
-        })?;
-
-        Ok(session)
-    });
-    task
-}
-
 #[derive(Clone)]
 pub struct DapAdapterDelegate {
     fs: Arc<dyn Fs>,

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

@@ -28,7 +28,6 @@ use gpui::{
     Task, WeakEntity,
 };
 
-use rpc::AnyProtoClient;
 use serde_json::{Value, json};
 use smol::stream::StreamExt;
 use std::any::TypeId;
@@ -115,54 +114,14 @@ impl From<dap::Thread> for Thread {
     }
 }
 
-type UpstreamProjectId = u64;
-
-struct RemoteConnection {
-    _client: AnyProtoClient,
-    _upstream_project_id: UpstreamProjectId,
-    _adapter_name: SharedString,
-}
-
-impl RemoteConnection {
-    fn send_proto_client_request<R: DapCommand>(
-        &self,
-        _request: R,
-        _session_id: SessionId,
-        cx: &mut App,
-    ) -> Task<Result<R::Response>> {
-        // let message = request.to_proto(session_id, self.upstream_project_id);
-        // let upstream_client = self.client.clone();
-        cx.background_executor().spawn(async move {
-            // debugger(todo): Properly send messages when we wrap dap_commands in envelopes again
-            // let response = upstream_client.request(message).await?;
-            // request.response_from_proto(response)
-            Err(anyhow!("Sending dap commands over RPC isn't supported yet"))
-        })
-    }
-
-    fn request<R: DapCommand>(
-        &self,
-        request: R,
-        session_id: SessionId,
-        cx: &mut App,
-    ) -> Task<Result<R::Response>>
-    where
-        <R::DapRequest as dap::requests::Request>::Response: 'static,
-        <R::DapRequest as dap::requests::Request>::Arguments: 'static + Send,
-    {
-        return self.send_proto_client_request::<R>(request, session_id, cx);
-    }
-}
-
 enum Mode {
-    Local(LocalMode),
-    Remote(RemoteConnection),
+    Building,
+    Running(LocalMode),
 }
 
 #[derive(Clone)]
 pub struct LocalMode {
     client: Arc<DebugAdapterClient>,
-    definition: DebugTaskDefinition,
     binary: DebugAdapterBinary,
     root_binary: Option<Arc<DebugAdapterBinary>>,
     pub(crate) breakpoint_store: Entity<BreakpointStore>,
@@ -186,56 +145,47 @@ fn client_source(abs_path: &Path) -> dap::Source {
 }
 
 impl LocalMode {
-    fn new(
+    async fn new(
         session_id: SessionId,
         parent_session: Option<Entity<Session>>,
         worktree: WeakEntity<Worktree>,
         breakpoint_store: Entity<BreakpointStore>,
-        config: DebugTaskDefinition,
         binary: DebugAdapterBinary,
         messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
         cx: AsyncApp,
-    ) -> Task<Result<Self>> {
-        cx.spawn(async move |cx| {
-            let message_handler = Box::new(move |message| {
-                messages_tx.unbounded_send(message).ok();
-            });
+    ) -> Result<Self> {
+        let message_handler = Box::new(move |message| {
+            messages_tx.unbounded_send(message).ok();
+        });
 
-            let root_binary = if let Some(parent_session) = parent_session.as_ref() {
-                Some(parent_session.read_with(cx, |session, _| session.root_binary().clone())?)
-            } else {
-                None
-            };
+        let root_binary = if let Some(parent_session) = parent_session.as_ref() {
+            Some(parent_session.read_with(&cx, |session, _| session.root_binary().clone())?)
+        } else {
+            None
+        };
 
-            let client = Arc::new(
-                if let Some(client) = parent_session
-                    .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
-                    .flatten()
-                {
-                    client
-                        .reconnect(session_id, binary.clone(), message_handler, cx.clone())
-                        .await?
-                } else {
-                    DebugAdapterClient::start(
-                        session_id,
-                        binary.clone(),
-                        message_handler,
-                        cx.clone(),
-                    )
+        let client = Arc::new(
+            if let Some(client) = parent_session
+                .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
+                .flatten()
+            {
+                client
+                    .reconnect(session_id, binary.clone(), message_handler, cx.clone())
+                    .await?
+            } else {
+                DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx.clone())
                     .await
                     .with_context(|| "Failed to start communication with debug adapter")?
-                },
-            );
+            },
+        );
 
-            Ok(Self {
-                client,
-                breakpoint_store,
-                worktree,
-                tmp_breakpoint: None,
-                definition: config,
-                root_binary,
-                binary,
-            })
+        Ok(Self {
+            client,
+            breakpoint_store,
+            worktree,
+            tmp_breakpoint: None,
+            root_binary,
+            binary,
         })
     }
 
@@ -371,19 +321,10 @@ impl LocalMode {
         })
     }
 
-    pub fn label(&self) -> String {
-        self.definition.label.clone()
-    }
-
-    fn request_initialization(&self, cx: &App) -> Task<Result<Capabilities>> {
-        let adapter_id = self.definition.adapter.clone();
-
-        self.request(Initialize { adapter_id }, cx.background_executor().clone())
-    }
-
     fn initialize_sequence(
         &self,
         capabilities: &Capabilities,
+        definition: &DebugTaskDefinition,
         initialized_rx: oneshot::Receiver<()>,
         dap_store: WeakEntity<DapStore>,
         cx: &App,
@@ -391,7 +332,7 @@ impl LocalMode {
         let mut raw = self.binary.request_args.clone();
 
         merge_json_value_into(
-            self.definition.initialize_args.clone().unwrap_or(json!({})),
+            definition.initialize_args.clone().unwrap_or(json!({})),
             &mut raw.configuration,
         );
 
@@ -426,9 +367,9 @@ impl LocalMode {
         let supports_exception_filters = capabilities
             .supports_exception_filter_options
             .unwrap_or_default();
+        let this = self.clone();
+        let worktree = self.worktree().clone();
         let configuration_sequence = cx.spawn({
-            let this = self.clone();
-            let worktree = self.worktree().clone();
             async move |cx| {
                 initialized_rx.await?;
                 let errors_by_path = cx
@@ -511,16 +452,10 @@ impl LocalMode {
         })
     }
 }
-impl From<RemoteConnection> for Mode {
-    fn from(value: RemoteConnection) -> Self {
-        Self::Remote(value)
-    }
-}
 
 impl Mode {
     fn request_dap<R: DapCommand>(
         &self,
-        session_id: SessionId,
         request: R,
         cx: &mut Context<Session>,
     ) -> Task<Result<R::Response>>
@@ -529,10 +464,13 @@ impl Mode {
         <R::DapRequest as dap::requests::Request>::Arguments: 'static + Send,
     {
         match self {
-            Mode::Local(debug_adapter_client) => {
+            Mode::Running(debug_adapter_client) => {
                 debug_adapter_client.request(request, cx.background_executor().clone())
             }
-            Mode::Remote(remote_connection) => remote_connection.request(request, session_id, cx),
+            Mode::Building => Task::ready(Err(anyhow!(
+                "no adapter running to send request: {:?}",
+                request
+            ))),
         }
     }
 }
@@ -609,10 +547,11 @@ pub struct OutputToken(pub usize);
 /// Represents a current state of a single debug adapter and provides ways to mutate it.
 pub struct Session {
     mode: Mode,
+    definition: DebugTaskDefinition,
     pub(super) capabilities: Capabilities,
     id: SessionId,
     child_session_ids: HashSet<SessionId>,
-    parent_id: Option<SessionId>,
+    parent_session: Option<Entity<Session>>,
     ignore_breakpoints: bool,
     modules: Vec<dap::Module>,
     loaded_sources: Vec<dap::Source>,
@@ -626,7 +565,8 @@ pub struct Session {
     is_session_terminated: bool,
     requests: HashMap<TypeId, HashMap<RequestSlot, Shared<Task<Option<()>>>>>,
     exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
-    _background_tasks: Vec<Task<()>>,
+    start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
+    background_tasks: Vec<Task<()>>,
 }
 
 trait CacheableCommand: Any + Send + Sync {
@@ -708,9 +648,12 @@ pub enum SessionEvent {
     StackTrace,
     Variables,
     Threads,
+    CapabilitiesLoaded,
 }
 
-pub(super) enum SessionStateEvent {
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SessionStateEvent {
+    Running,
     Shutdown,
     Restart,
 }
@@ -722,80 +665,140 @@ impl EventEmitter<SessionStateEvent> for Session {}
 // remote side will only send breakpoint updates when it is a breakpoint created by that peer
 // BreakpointStore notifies session on breakpoint changes
 impl Session {
-    pub(crate) fn local(
+    pub(crate) fn new(
         breakpoint_store: Entity<BreakpointStore>,
-        worktree: WeakEntity<Worktree>,
         session_id: SessionId,
         parent_session: Option<Entity<Session>>,
-        binary: DebugAdapterBinary,
-        config: DebugTaskDefinition,
+        template: DebugTaskDefinition,
         start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
-        initialized_tx: oneshot::Sender<()>,
         cx: &mut App,
-    ) -> Task<Result<Entity<Self>>> {
-        let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
+    ) -> Entity<Self> {
+        cx.new::<Self>(|cx| {
+            cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
+                BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
+                    if let Some(local) = (!this.ignore_breakpoints)
+                        .then(|| this.as_local_mut())
+                        .flatten()
+                    {
+                        local
+                            .send_breakpoints_from_path(path.clone(), *reason, cx)
+                            .detach();
+                    };
+                }
+                BreakpointStoreEvent::BreakpointsCleared(paths) => {
+                    if let Some(local) = (!this.ignore_breakpoints)
+                        .then(|| this.as_local_mut())
+                        .flatten()
+                    {
+                        local.unset_breakpoints_from_paths(paths, cx).detach();
+                    }
+                }
+                BreakpointStoreEvent::ActiveDebugLineChanged => {}
+            })
+            .detach();
+
+            let this = Self {
+                mode: Mode::Building,
+                id: session_id,
+                child_session_ids: HashSet::default(),
+                parent_session,
+                capabilities: Capabilities::default(),
+                ignore_breakpoints: false,
+                variables: Default::default(),
+                stack_frames: Default::default(),
+                thread_states: ThreadStates::default(),
+                output_token: OutputToken(0),
+                output: circular_buffer::CircularBuffer::boxed(),
+                requests: HashMap::default(),
+                modules: Vec::default(),
+                loaded_sources: Vec::default(),
+                threads: IndexMap::default(),
+                background_tasks: Vec::default(),
+                locations: Default::default(),
+                is_session_terminated: false,
+                exception_breakpoints: Default::default(),
+                definition: template,
+                start_debugging_requests_tx,
+            };
+
+            this
+        })
+    }
+
+    pub fn worktree(&self) -> Option<Entity<Worktree>> {
+        match &self.mode {
+            Mode::Building => None,
+            Mode::Running(local_mode) => local_mode.worktree.upgrade(),
+        }
+    }
 
-        cx.spawn(async move |cx| {
+    pub fn boot(
+        &mut self,
+        binary: DebugAdapterBinary,
+        worktree: Entity<Worktree>,
+        breakpoint_store: Entity<BreakpointStore>,
+        dap_store: WeakEntity<DapStore>,
+        cx: &mut Context<Self>,
+    ) -> 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);
+            while let Some(message) = message_rx.next().await {
+                if let Message::Event(event) = message {
+                    if let Events::Initialized(_) = *event {
+                        if let Some(tx) = initialized_tx.take() {
+                            tx.send(()).ok();
+                        }
+                    } else {
+                        let Ok(_) = this.update(cx, |session, cx| {
+                            session.handle_dap_event(event, cx);
+                        }) else {
+                            break;
+                        };
+                    }
+                } else {
+                    let Ok(Ok(_)) = this.update(cx, |this, _| {
+                        this.start_debugging_requests_tx
+                            .unbounded_send((session_id, message))
+                    }) else {
+                        break;
+                    };
+                }
+            }
+        })];
+        self.background_tasks = background_tasks;
+        let id = self.id;
+        let parent_session = self.parent_session.clone();
+
+        cx.spawn(async move |this, cx| {
             let mode = LocalMode::new(
-                session_id,
-                parent_session.clone(),
-                worktree,
+                id,
+                parent_session,
+                worktree.downgrade(),
                 breakpoint_store.clone(),
-                config.clone(),
                 binary,
                 message_tx,
                 cx.clone(),
             )
             .await?;
-
-            cx.new(|cx| {
-                create_local_session(
-                    breakpoint_store,
-                    session_id,
-                    parent_session,
-                    start_debugging_requests_tx,
-                    initialized_tx,
-                    message_rx,
-                    mode,
-                    cx,
-                )
-            })
+            this.update(cx, |this, cx| {
+                this.mode = Mode::Running(mode);
+                cx.emit(SessionStateEvent::Running);
+            })?;
+
+            this.update(cx, |session, cx| session.request_initialize(cx))?
+                .await?;
+
+            this.update(cx, |session, cx| {
+                session.initialize_sequence(initialized_rx, dap_store.clone(), cx)
+            })?
+            .await
         })
     }
 
-    pub(crate) fn remote(
-        session_id: SessionId,
-        client: AnyProtoClient,
-        upstream_project_id: u64,
-        ignore_breakpoints: bool,
-    ) -> Self {
-        Self {
-            mode: Mode::Remote(RemoteConnection {
-                _adapter_name: SharedString::new(""), // todo(debugger) we need to pipe in the right values to deserialize the debugger pane layout
-                _client: client,
-                _upstream_project_id: upstream_project_id,
-            }),
-            id: session_id,
-            child_session_ids: HashSet::default(),
-            parent_id: None,
-            capabilities: Capabilities::default(),
-            ignore_breakpoints,
-            variables: Default::default(),
-            stack_frames: Default::default(),
-            thread_states: ThreadStates::default(),
-            output_token: OutputToken(0),
-            output: circular_buffer::CircularBuffer::boxed(),
-            requests: HashMap::default(),
-            modules: Vec::default(),
-            loaded_sources: Vec::default(),
-            threads: IndexMap::default(),
-            _background_tasks: Vec::default(),
-            locations: Default::default(),
-            is_session_terminated: false,
-            exception_breakpoints: Default::default(),
-        }
-    }
-
     pub fn session_id(&self) -> SessionId {
         self.id
     }
@@ -812,8 +815,14 @@ impl Session {
         self.child_session_ids.remove(&session_id);
     }
 
-    pub fn parent_id(&self) -> Option<SessionId> {
-        self.parent_id
+    pub fn parent_id(&self, cx: &App) -> Option<SessionId> {
+        self.parent_session
+            .as_ref()
+            .map(|session| session.read(cx).id)
+    }
+
+    pub fn parent_session(&self) -> Option<&Entity<Self>> {
+        self.parent_session.as_ref()
     }
 
     pub fn capabilities(&self) -> &Capabilities {
@@ -821,35 +830,35 @@ impl Session {
     }
 
     pub(crate) fn root_binary(&self) -> Arc<DebugAdapterBinary> {
-        let Mode::Local(local_mode) = &self.mode else {
-            panic!("Session is not local");
-        };
-        local_mode
-            .root_binary
-            .clone()
-            .unwrap_or_else(|| Arc::new(local_mode.binary.clone()))
+        match &self.mode {
+            Mode::Building => {
+                // todo(debugger): Implement root_binary for building mode
+                unimplemented!()
+            }
+            Mode::Running(running) => running
+                .root_binary
+                .clone()
+                .unwrap_or_else(|| Arc::new(running.binary.clone())),
+        }
     }
 
     pub fn binary(&self) -> &DebugAdapterBinary {
-        let Mode::Local(local_mode) = &self.mode else {
+        let Mode::Running(local_mode) = &self.mode else {
             panic!("Session is not local");
         };
         &local_mode.binary
     }
 
     pub fn adapter_name(&self) -> SharedString {
-        match &self.mode {
-            Mode::Local(local_mode) => local_mode.definition.adapter.clone().into(),
-            Mode::Remote(remote_mode) => remote_mode._adapter_name.clone(),
-        }
+        self.definition.adapter.clone().into()
     }
 
-    pub fn configuration(&self) -> Option<DebugTaskDefinition> {
-        if let Mode::Local(local_mode) = &self.mode {
-            Some(local_mode.definition.clone())
-        } else {
-            None
-        }
+    pub fn label(&self) -> String {
+        self.definition.label.clone()
+    }
+
+    pub fn definition(&self) -> DebugTaskDefinition {
+        self.definition.clone()
     }
 
     pub fn is_terminated(&self) -> bool {
@@ -857,31 +866,33 @@ impl Session {
     }
 
     pub fn is_local(&self) -> bool {
-        matches!(self.mode, Mode::Local(_))
+        matches!(self.mode, Mode::Running(_))
     }
 
     pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
         match &mut self.mode {
-            Mode::Local(local_mode) => Some(local_mode),
-            Mode::Remote(_) => None,
+            Mode::Running(local_mode) => Some(local_mode),
+            Mode::Building => None,
         }
     }
 
     pub fn as_local(&self) -> Option<&LocalMode> {
         match &self.mode {
-            Mode::Local(local_mode) => Some(local_mode),
-            Mode::Remote(_) => None,
+            Mode::Running(local_mode) => Some(local_mode),
+            Mode::Building => None,
         }
     }
 
     pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+        let adapter_id = self.definition.adapter.clone();
+        let request = Initialize { adapter_id };
         match &self.mode {
-            Mode::Local(local_mode) => {
-                let capabilities = local_mode.clone().request_initialization(cx);
+            Mode::Running(local_mode) => {
+                let capabilities = local_mode.request(request, cx.background_executor().clone());
 
                 cx.spawn(async move |this, cx| {
                     let capabilities = capabilities.await?;
-                    this.update(cx, |session, _| {
+                    this.update(cx, |session, cx| {
                         session.capabilities = capabilities;
                         let filters = session
                             .capabilities
@@ -895,12 +906,13 @@ impl Session {
                                 .entry(filter.filter.clone())
                                 .or_insert_with(|| (filter, default));
                         }
+                        cx.emit(SessionEvent::CapabilitiesLoaded);
                     })?;
                     Ok(())
                 })
             }
-            Mode::Remote(_) => Task::ready(Err(anyhow!(
-                "Cannot send initialize request from remote session"
+            Mode::Building => Task::ready(Err(anyhow!(
+                "Cannot send initialize request, task still building"
             ))),
         }
     }
@@ -912,10 +924,14 @@ impl Session {
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         match &self.mode {
-            Mode::Local(local_mode) => {
-                local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
-            }
-            Mode::Remote(_) => Task::ready(Err(anyhow!("cannot initialize remote session"))),
+            Mode::Running(local_mode) => local_mode.initialize_sequence(
+                &self.capabilities,
+                &self.definition,
+                initialize_rx,
+                dap_store,
+                cx,
+            ),
+            Mode::Building => Task::ready(Err(anyhow!("cannot initialize, still building"))),
         }
     }
 
@@ -926,7 +942,7 @@ impl Session {
         cx: &mut Context<Self>,
     ) {
         match &mut self.mode {
-            Mode::Local(local_mode) => {
+            Mode::Running(local_mode) => {
                 if !matches!(
                     self.thread_states.thread_state(active_thread_id),
                     Some(ThreadStatus::Stopped)
@@ -949,7 +965,7 @@ impl Session {
                 })
                 .detach();
             }
-            Mode::Remote(_) => {}
+            Mode::Building => {}
         }
     }
 
@@ -983,13 +999,13 @@ impl Session {
         body: Option<serde_json::Value>,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        let Some(local_session) = self.as_local().cloned() else {
+        let Some(local_session) = self.as_local() else {
             unreachable!("Cannot respond to remote client");
         };
+        let client = local_session.client.clone();
 
         cx.background_spawn(async move {
-            local_session
-                .client
+            client
                 .send_message(Message::Response(Response {
                     body,
                     success,
@@ -1178,7 +1194,6 @@ impl Session {
 
             let task = Self::request_inner::<Arc<T>>(
                 &self.capabilities,
-                self.id,
                 &self.mode,
                 command,
                 process_result,
@@ -1199,7 +1214,6 @@ impl Session {
 
     fn request_inner<T: DapCommand + PartialEq + Eq + Hash>(
         capabilities: &Capabilities,
-        session_id: SessionId,
         mode: &Mode,
         request: T,
         process_result: impl FnOnce(
@@ -1225,7 +1239,7 @@ impl Session {
             });
         }
 
-        let request = mode.request_dap(session_id, request, cx);
+        let request = mode.request_dap(request, cx);
         cx.spawn(async move |this, cx| {
             let result = request.await;
             this.update(cx, |this, cx| process_result(this, result, cx))
@@ -1245,14 +1259,7 @@ impl Session {
         + 'static,
         cx: &mut Context<Self>,
     ) -> Task<Option<T::Response>> {
-        Self::request_inner(
-            &self.capabilities,
-            self.id,
-            &self.mode,
-            request,
-            process_result,
-            cx,
-        )
+        Self::request_inner(&self.capabilities, &self.mode, request, process_result, cx)
     }
 
     fn invalidate_command_type<Command: DapCommand>(&mut self) {
@@ -1569,8 +1576,8 @@ impl Session {
 
     pub fn adapter_client(&self) -> Option<Arc<DebugAdapterClient>> {
         match self.mode {
-            Mode::Local(ref local) => Some(local.client.clone()),
-            Mode::Remote(_) => None,
+            Mode::Running(ref local) => Some(local.client.clone()),
+            Mode::Building => None,
         }
     }
 
@@ -1936,83 +1943,3 @@ impl Session {
         }
     }
 }
-
-fn create_local_session(
-    breakpoint_store: Entity<BreakpointStore>,
-    session_id: SessionId,
-    parent_session: Option<Entity<Session>>,
-    start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
-    initialized_tx: oneshot::Sender<()>,
-    mut message_rx: futures::channel::mpsc::UnboundedReceiver<Message>,
-    mode: LocalMode,
-    cx: &mut Context<Session>,
-) -> Session {
-    let _background_tasks = vec![cx.spawn(async move |this: WeakEntity<Session>, cx| {
-        let mut initialized_tx = Some(initialized_tx);
-        while let Some(message) = message_rx.next().await {
-            if let Message::Event(event) = message {
-                if let Events::Initialized(_) = *event {
-                    if let Some(tx) = initialized_tx.take() {
-                        tx.send(()).ok();
-                    }
-                } else {
-                    let Ok(_) = this.update(cx, |session, cx| {
-                        session.handle_dap_event(event, cx);
-                    }) else {
-                        break;
-                    };
-                }
-            } else {
-                let Ok(_) = start_debugging_requests_tx.unbounded_send((session_id, message))
-                else {
-                    break;
-                };
-            }
-        }
-    })];
-
-    cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
-        BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
-            if let Some(local) = (!this.ignore_breakpoints)
-                .then(|| this.as_local_mut())
-                .flatten()
-            {
-                local
-                    .send_breakpoints_from_path(path.clone(), *reason, cx)
-                    .detach();
-            };
-        }
-        BreakpointStoreEvent::BreakpointsCleared(paths) => {
-            if let Some(local) = (!this.ignore_breakpoints)
-                .then(|| this.as_local_mut())
-                .flatten()
-            {
-                local.unset_breakpoints_from_paths(paths, cx).detach();
-            }
-        }
-        BreakpointStoreEvent::ActiveDebugLineChanged => {}
-    })
-    .detach();
-
-    Session {
-        mode: Mode::Local(mode),
-        id: session_id,
-        child_session_ids: HashSet::default(),
-        parent_id: parent_session.map(|session| session.read(cx).id),
-        variables: Default::default(),
-        capabilities: Capabilities::default(),
-        thread_states: ThreadStates::default(),
-        output_token: OutputToken(0),
-        ignore_breakpoints: false,
-        output: circular_buffer::CircularBuffer::boxed(),
-        requests: HashMap::default(),
-        modules: Vec::default(),
-        loaded_sources: Vec::default(),
-        threads: IndexMap::default(),
-        stack_frames: IndexMap::default(),
-        locations: Default::default(),
-        exception_breakpoints: Default::default(),
-        _background_tasks,
-        is_session_terminated: false,
-    }
-}

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

@@ -1,68 +1,39 @@
 use std::{path::Path, sync::Arc};
 
-use anyhow::Result;
-use dap::{DebugRequest, client::DebugAdapterClient};
-use gpui::{App, AppContext, Entity, Subscription, Task};
-use task::DebugTaskDefinition;
+use dap::client::DebugAdapterClient;
+use gpui::{App, AppContext, Subscription};
 
-use crate::Project;
-
-use super::session::Session;
+use super::session::{Session, SessionStateEvent};
 
 pub fn intercept_debug_sessions<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
     cx: &mut gpui::TestAppContext,
     configure: T,
 ) -> Subscription {
     cx.update(|cx| {
-        cx.observe_new::<Session>(move |session, _, cx| {
-            let client = session.adapter_client().unwrap();
-            register_default_handlers(session, &client, cx);
-            configure(&client);
-            cx.background_spawn(async move {
-                client
-                    .fake_event(dap::messages::Events::Initialized(Some(Default::default())))
-                    .await
+        let configure = Arc::new(configure);
+        cx.observe_new::<Session>(move |_, _, cx| {
+            let configure = configure.clone();
+            cx.subscribe_self(move |session, event, cx| {
+                let configure = configure.clone();
+                if matches!(event, SessionStateEvent::Running) {
+                    let client = session.adapter_client().unwrap();
+                    register_default_handlers(session, &client, cx);
+                    configure(&client);
+                    cx.background_spawn(async move {
+                        client
+                            .fake_event(dap::messages::Events::Initialized(
+                                Some(Default::default()),
+                            ))
+                            .await
+                    })
+                    .detach();
+                }
             })
             .detach();
         })
     })
 }
 
-pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
-    project: &Entity<Project>,
-    cx: &mut gpui::TestAppContext,
-    config: DebugTaskDefinition,
-    configure: T,
-) -> Task<Result<Entity<Session>>> {
-    let subscription = intercept_debug_sessions(cx, configure);
-    let task = project.update(cx, |project, cx| project.start_debug_session(config, cx));
-    cx.spawn(async move |_| {
-        let result = task.await;
-        drop(subscription);
-        result
-    })
-}
-
-pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
-    project: &Entity<Project>,
-    cx: &mut gpui::TestAppContext,
-    configure: T,
-) -> Task<Result<Entity<Session>>> {
-    start_debug_session_with(
-        project,
-        cx,
-        DebugTaskDefinition {
-            adapter: "fake-adapter".to_string(),
-            request: DebugRequest::Launch(Default::default()),
-            label: "test".to_string(),
-            initialize_args: None,
-            tcp_connection: None,
-            stop_on_entry: None,
-        },
-        configure,
-    )
-}
-
 fn register_default_handlers(session: &Session, client: &Arc<DebugAdapterClient>, cx: &mut App) {
     client.on_request::<dap::requests::Initialize, _>(move |_, _| Ok(Default::default()));
     let paths = session

crates/project/src/project.rs 🔗

@@ -25,7 +25,6 @@ mod environment;
 use buffer_diff::BufferDiff;
 pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
 use git_store::{Repository, RepositoryId};
-use task::DebugTaskDefinition;
 pub mod search_history;
 mod yarn;
 
@@ -39,17 +38,13 @@ use client::{
 };
 use clock::ReplicaId;
 
-use dap::{
-    adapters::{DebugAdapterBinary, TcpArguments},
-    client::DebugAdapterClient,
-};
+use dap::client::DebugAdapterClient;
 
 use collections::{BTreeSet, HashMap, HashSet};
 use debounced_delay::DebouncedDelay;
 use debugger::{
     breakpoint_store::BreakpointStore,
     dap_store::{DapStore, DapStoreEvent},
-    session::Session,
 };
 pub use environment::ProjectEnvironment;
 #[cfg(test)]
@@ -97,7 +92,6 @@ use snippet::Snippet;
 use snippet_provider::SnippetProvider;
 use std::{
     borrow::Cow,
-    net::Ipv4Addr,
     ops::Range,
     path::{Component, Path, PathBuf},
     pin::pin,
@@ -107,7 +101,7 @@ use std::{
 };
 
 use task_store::TaskStore;
-use terminals::{SshCommand, Terminals, wrap_for_ssh};
+use terminals::Terminals;
 use text::{Anchor, BufferId};
 use toolchain_store::EmptyToolchainStore;
 use util::{
@@ -1072,8 +1066,9 @@ impl Project {
             let dap_store = cx.new(|cx| {
                 DapStore::new_ssh(
                     SSH_PROJECT_ID,
-                    ssh_proto.clone(),
+                    ssh.clone(),
                     breakpoint_store.clone(),
+                    worktree_store.clone(),
                     cx,
                 )
             });
@@ -1258,6 +1253,7 @@ impl Project {
                 remote_id,
                 client.clone().into(),
                 breakpoint_store.clone(),
+                worktree_store.clone(),
                 cx,
             )
         })?;
@@ -1463,79 +1459,6 @@ impl Project {
         }
     }
 
-    pub fn start_debug_session(
-        &mut self,
-        definition: DebugTaskDefinition,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<Session>>> {
-        let Some(worktree) = self.worktrees(cx).find(|tree| tree.read(cx).is_visible()) else {
-            return Task::ready(Err(anyhow!("Failed to find a worktree")));
-        };
-
-        let ssh_client = self.ssh_client().clone();
-
-        let result = cx.spawn(async move |this, cx| {
-            let mut binary = this
-                .update(cx, |this, cx| {
-                    this.dap_store.update(cx, |dap_store, cx| {
-                        dap_store.get_debug_adapter_binary(definition.clone(), cx)
-                    })
-                })?
-                .await?;
-
-            if let Some(ssh_client) = ssh_client {
-                let mut ssh_command = ssh_client.update(cx, |ssh, _| {
-                    anyhow::Ok(SshCommand {
-                        arguments: ssh
-                            .ssh_args()
-                            .ok_or_else(|| anyhow!("SSH arguments not found"))?,
-                    })
-                })??;
-
-                let mut connection = None;
-                if let Some(c) = binary.connection {
-                    let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
-                    let port = dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
-
-                    ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
-                    connection = Some(TcpArguments {
-                        port: c.port,
-                        host: local_bind_addr,
-                        timeout: c.timeout,
-                    })
-                }
-
-                let (program, args) = wrap_for_ssh(
-                    &ssh_command,
-                    Some((&binary.command, &binary.arguments)),
-                    binary.cwd.as_deref(),
-                    binary.envs,
-                    None,
-                );
-
-                binary = DebugAdapterBinary {
-                    command: program,
-                    arguments: args,
-                    envs: HashMap::default(),
-                    cwd: None,
-                    connection,
-                    request_args: binary.request_args,
-                }
-            };
-
-            let ret = this
-                .update(cx, |project, cx| {
-                    project.dap_store.update(cx, |dap_store, cx| {
-                        dap_store.new_session(binary, definition, worktree.downgrade(), None, cx)
-                    })
-                })?
-                .1
-                .await;
-            ret
-        });
-        result
-    }
-
     #[cfg(any(test, feature = "test-support"))]
     pub async fn example(
         root_paths: impl IntoIterator<Item = &Path>,

crates/workspace/src/tasks.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
 use gpui::{Context, Task};
 use project::TaskSourceKind;
 use remote::ConnectionState;
-use task::{ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
+use task::{DebugTaskDefinition, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
 use ui::Window;
 
 use crate::Workspace;
@@ -109,14 +109,26 @@ impl Workspace {
                 debug_config.definition
             };
 
-            project
-                .update(cx, |project, cx| project.start_debug_session(config, cx))?
-                .await?;
+            workspace.update_in(cx, |workspace, window, cx| {
+                workspace.start_debug_session(config, window, cx);
+            })?;
+
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);
     }
 
+    pub fn start_debug_session(
+        &mut self,
+        definition: DebugTaskDefinition,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(provider) = self.debugger_provider.as_mut() {
+            provider.start_session(definition, window, cx)
+        }
+    }
+
     pub fn spawn_in_terminal(
         self: &mut Workspace,
         spawn_in_terminal: SpawnInTerminal,

crates/workspace/src/workspace.rs 🔗

@@ -96,7 +96,7 @@ use std::{
     sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
     time::Duration,
 };
-use task::SpawnInTerminal;
+use task::{DebugTaskDefinition, SpawnInTerminal};
 use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
@@ -139,6 +139,10 @@ pub trait TerminalProvider {
     ) -> Task<Result<ExitStatus>>;
 }
 
+pub trait DebuggerProvider {
+    fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App);
+}
+
 actions!(
     workspace,
     [
@@ -860,6 +864,7 @@ pub struct Workspace {
     on_prompt_for_new_path: Option<PromptForNewPath>,
     on_prompt_for_open_path: Option<PromptForOpenPath>,
     terminal_provider: Option<Box<dyn TerminalProvider>>,
+    debugger_provider: Option<Box<dyn DebuggerProvider>>,
     serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
     serialized_ssh_project: Option<SerializedSshProject>,
     _items_serializer: Task<Result<()>>,
@@ -1186,6 +1191,7 @@ impl Workspace {
             on_prompt_for_new_path: None,
             on_prompt_for_open_path: None,
             terminal_provider: None,
+            debugger_provider: None,
             serializable_items_tx,
             _items_serializer,
             session_id: Some(session_id),
@@ -1705,6 +1711,10 @@ impl Workspace {
         self.terminal_provider = Some(Box::new(provider));
     }
 
+    pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
+        self.debugger_provider = Some(Box::new(provider));
+    }
+
     pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
         self.serialized_ssh_project.clone()
     }

crates/zed/src/zed.rs 🔗

@@ -444,7 +444,7 @@ fn initialize_panels(
                     window,
                     async move |workspace: gpui::WeakEntity<Workspace>,
                                 cx: &mut AsyncWindowContext| {
-                        let debug_panel = DebugPanel::load(workspace.clone(), cx.clone()).await?;
+                        let debug_panel = DebugPanel::load(workspace.clone(), cx).await?;
                         workspace.update_in(cx, |workspace, window, cx| {
                             workspace.add_panel(debug_panel, window, cx);
                         })?;