From 6a009b447af415f80cdb55d8c6064e984b899fd5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 22 Apr 2025 17:35:47 -0600 Subject: [PATCH] debugger: Open debugger panel on session startup (#29186) 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 Co-authored-by: Anthony Co-authored-by: Cole Miller Co-authored-by: Cole Miller Co-authored-by: Zed AI Co-authored-by: Remco Smits --- Cargo.lock | 3 + crates/collab/Cargo.toml | 1 + crates/collab/src/tests/debug_panel_tests.rs | 6 +- .../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 | 332 +++++++---- 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 ++++ .../debugger_ui/src/tests/debugger_panel.rs | 131 +++-- crates/debugger_ui/src/tests/module_list.rs | 10 +- .../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 | 409 ++++++-------- crates/project/src/debugger/session.rs | 515 ++++++++---------- 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, 1261 insertions(+), 1021 deletions(-) create mode 100644 crates/debugger_ui/src/tests/dap_logger.rs diff --git a/Cargo.lock b/Cargo.lock index cf8833968b..75594ed5ac 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 4d6787f857..b446e264ac 100644 --- a/crates/collab/Cargo.toml +++ b/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 diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index a8e9b745ac..9f42f8c69e 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/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}; diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 09e712b46d..cfc7df7d5b 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/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::(cx)) + .unwrap(); + + let workspace_window = cx_a + .window_handle() + .downcast::() + .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(); +} diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 68e9d5a4b3..1e2a8badaf 100644 --- a/crates/dap/src/transport.rs +++ b/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>>>, + _tasks: Vec>>, } 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); diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index 6639a71a67..d91f43182d 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/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 diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index cfe96115f6..acd26e2d7f 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -41,7 +41,7 @@ struct DapLogView { _subscriptions: Vec, } -struct LogStore { +pub struct LogStore { projects: HashMap, ProjectState>, debug_clients: HashMap, rpc_tx: UnboundedSender<(SessionId, IoKind, String)>, @@ -101,7 +101,7 @@ impl DebugAdapterState { } impl LogStore { - fn new(cx: &Context) -> Self { + pub fn new(cx: &Context) -> 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 for LogStore {} impl EventEmitter for DapLogView {} impl EventEmitter for DapLogView {} impl EventEmitter for DapLogView {} + +#[cfg(any(test, feature = "test-support"))] +impl LogStore { + pub fn contained_session_ids(&self) -> Vec { + self.debug_clients.keys().cloned().collect() + } + + pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec { + 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 { + self.debug_clients + .get(&session_id) + .expect("This session should exist if a test is calling") + .log_messages + .clone() + .into() + } +} diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ac180f308f..a195b10801 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/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"] } diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index a09a9bac36..d5ccf6c9ba 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/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, placeholder_text: Arc, - project: Entity, + workspace: WeakEntity, pub(crate) debug_config: task::DebugTaskDefinition, candidates: Arc<[Candidate]>, } impl AttachModalDelegate { fn new( - project: Entity, + workspace: Entity, 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, + workspace: Entity, 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, + workspace: Entity, 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>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { 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::(cx)) + .ok() + .flatten(); + if let Some(panel) = panel { + panel.update(cx, |panel, cx| { + panel.start_session(definition, window, cx); + }); + } cx.emit(DismissEvent); } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b19a3633c7..6ddf563f06 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/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>, /// This represents the last debug definition that was created in the new session modal pub(crate) past_debug_definition: Option, - project: WeakEntity, + project: Entity, workspace: WeakEntity, focus_handle: FocusHandle, context_menu: Option<(Entity, Point, 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, - cx: AsyncWindowContext, + cx: &mut AsyncWindowContext, ) -> Task>> { 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 start_session( + &mut self, + definition: DebugTaskDefinition, + window: &mut Window, + cx: &mut Context, + ) { + 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) + }; + + (session.clone(), dap_store.boot_session(session, cx)) + })?; + + match task.await { + Err(e) => { + this.update(cx, |this, cx| { + this.workspace + .update(cx, |workspace, cx| { + workspace.show_error(&e, cx); + }) + .ok(); + }) + .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); + } + + async fn register_session( + this: WeakEntity, + session: Entity, + 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()) + { + curr_session = parent_session; + } + + let definition = curr_session.update(cx, |session, _| session.definition()); + let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); + + 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::(window, cx); + })?; + Ok(()) + } + + pub fn start_child_session( + &mut self, + request: &StartDebuggingRequestArguments, + parent_session: Entity, + window: &mut Window, + cx: &mut Context, + ) { + 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(); + + 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(); + }) + .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> { self.active_session.clone() } - pub fn debug_panel_items_by_client( - &self, - client_id: &SessionId, - cx: &Context, - ) -> Vec> { - 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, - ) -> Option> { - self.sessions - .iter() - .find(|item| { - let item = item.read(cx); - - item.session_id(cx) == client_id - }) - .cloned() - } - fn handle_dap_store_event( &mut self, - dap_store: &Entity, + _dap_store: &Entity, event: &dap_store::DapStoreEvent, window: &mut Window, cx: &mut Context, ) { 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 adapter_name = session.read(cx).adapter_name(); - - let session_id = *session_id; - cx.spawn_in(window, async move |this, cx| { - let serialized_layout = - persistence::get_serialized_pane_layout(adapter_name).await; - - 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" - ); - }; - - if this - .sessions - .iter() - .any(|item| item.read(cx).session_id(cx) == session_id) - { - // We already have an item for this session. - debug_panic!("We should never reuse session ids"); - return; - } - - this.sessions.retain(|session| { - session - .read(cx) - .mode() - .as_running() - .map_or(false, |running_state| { - !running_state.read(cx).session().read(cx).is_terminated() - }) - }); - - let session_item = DebugSession::running( - project, - this.workspace.clone(), - 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); - }) - }) - .detach(); - } 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 for DebugPanel {} impl EventEmitter for DebugPanel {} -impl EventEmitter 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); + +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); + }) + }) + } +} diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 55eea2315e..ce3300de5f 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/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!( diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index cf59a7055b..dc473bead5 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/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 { + 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) -> Result<()> { - let workspace = self.workspace.clone(); - let config = self - .debug_config(cx) - .ok_or_else(|| anyhow!("Failed to create a debug config"))?; + fn start_new_session(&self, window: &mut Window, cx: &mut Context) { + 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 _ = self.debug_panel.update(cx, |panel, _| { - panel.past_debug_definition = Some(config.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, - project: Entity, + workspace: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -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, - project: Entity, + workspace: Entity, window: &mut Window, cx: &mut Context, ) -> Self { - Self::Attach(AttachMode::new(debugger, project, window, cx)) + Self::Attach(AttachMode::new(debugger, workspace, window, cx)) } fn launch( past_launch_config: Option, @@ -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()), ), diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 93fbdc1111..7862379413 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -88,6 +88,12 @@ impl DebugSession { } } + pub fn session(&self, cx: &App) -> Entity { + match &self.mode { + DebugSessionState::Running(entity) => entity.read(cx).session().clone(), + } + } + pub(crate) fn shutdown(&mut self, cx: &mut Context) { 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() } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 59766dc1ff..438ded2e36 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/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, ) { - 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, stack_frame_list: &Entity, variable_list: &Entity, - module_list: Option<&Entity>, - loaded_source_list: Option<&Entity>, + module_list: &Entity, + loaded_source_list: &Entity, console: &Entity, breakpoints: &Entity, subscriptions: &mut HashMap, @@ -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); diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 6fbe84a9e5..0305f09aad 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/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) + 'static>( + workspace: &WindowHandle, + cx: &mut gpui::TestAppContext, + config: DebugTaskDefinition, + configure: T, +) -> Result> { + 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::(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) + 'static>( + workspace: &WindowHandle, + cx: &mut gpui::TestAppContext, + configure: T, +) -> Result> { + 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, + ) +} diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 9266dcc46c..d5d2115376 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/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()), diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 1fe57b2906..534cf1320e 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/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::(move |_, _| { diff --git a/crates/debugger_ui/src/tests/dap_logger.rs b/crates/debugger_ui/src/tests/dap_logger.rs new file mode 100644 index 0000000000..3767d05073 --- /dev/null +++ b/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::(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::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }); + + client.on_request::(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(); +} diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 4985fccb15..60760dcb89 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/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::(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::(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::(move |_, args| { + launched_with.lock().replace(args.raw); + Ok(()) + }); + client.on_request::(move |_, _| { + assert!(false, "should not get attach request"); + Ok(()) + }); + } + }); + + client + .fake_reverse_request::(StartDebuggingRequestArguments { + request: StartDebuggingRequestArgumentsRequest::Launch, + configuration: fake_config.clone(), + }) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, _window, cx| { + let debug_panel = workspace.panel::(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::(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::(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::(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::(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::(|_, _| { 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(); diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 30c25e58b4..d2c0ca4c10 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/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::(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()); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 8f21e1d5b0..e6f0a4478f 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/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::(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::(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::(move |_, _| { diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index fa539babd8..5abb422ac7 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/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::(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::(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::(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::(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::(move |_, _| { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index cd3d874914..f5fddb75d9 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/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, sender: mpsc::Sender>, }, + SpawnChildSession { + request: StartDebuggingRequestArguments, + parent_session: Entity, + }, Notification(String), RemoteHasInitialized, } @@ -83,12 +90,12 @@ pub struct LocalDapStore { http_client: Arc, environment: Entity, language_registry: Arc, - worktree_store: Entity, toolchain_store: Arc, locators: HashMap>, } pub struct SshDapStore { + ssh_client: Entity, upstream_client: AnyProtoClient, upstream_project_id: u64, } @@ -97,6 +104,7 @@ pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, breakpoint_store: Entity, + worktree_store: Entity, sessions: BTreeMap>, 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, breakpoint_store: Entity, + worktree_store: Entity, cx: &mut Context, ) -> 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, + worktree_store: Entity, cx: &mut Context, ) -> Self { - Self::new(DapStoreMode::Collab, breakpoint_store, cx) + Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx) } fn new( mode: DapStoreMode, breakpoint_store: Entity, + worktree_store: Entity, cx: &mut Context, ) -> 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, ) -> Task> { 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, + template: DebugTaskDefinition, + parent_session: Option>, cx: &mut Context, - ) { - 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 { + 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, + cx: &mut Context, + ) -> Task> { + 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 { + &self.worktree_store + } + #[allow(dead_code)] async fn handle_ignore_breakpoint_state( this: Entity, @@ -407,52 +514,6 @@ impl DapStore { ) } - pub fn new_session( - &mut self, - binary: DebugAdapterBinary, - config: DebugTaskDefinition, - worktree: WeakEntity, - parent_session: Option>, - cx: &mut Context, - ) -> (SessionId, Task>>) { - 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 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::( - 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 new_session_task = self - .new_session(binary, config, worktree, Some(parent_session.clone()), cx) - .1; - 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, - }), - })?), - ), - }; + let launch_request: Option> = request + .arguments + .as_ref() + .map(|value| serde_json::from_value(value.clone())); + + let mut success = true; + if let Some(Ok(request)) = launch_request { + cx.emit(DapStoreEvent::SpawnChildSession { + request, + parent_session: parent_session.clone(), + }); + } else { + log::error!( + "Failed to parse launch request arguments: {:?}", + request.arguments + ); + success = false; + } + + cx.spawn(async move |_, cx| { parent_session .update(cx, |session, cx| { session.respond_to_client( request_seq, success, StartDebugging::COMMAND.to_string(), - 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, anyhow::Error>>, - worktree: WeakEntity, - cx: &mut Context, -) -> Task>> { - 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, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index c7ae32c6e1..bfa51335af 100644 --- a/crates/project/src/debugger/session.rs +++ b/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 for Thread { } } -type UpstreamProjectId = u64; - -struct RemoteConnection { - _client: AnyProtoClient, - _upstream_project_id: UpstreamProjectId, - _adapter_name: SharedString, -} - -impl RemoteConnection { - fn send_proto_client_request( - &self, - _request: R, - _session_id: SessionId, - cx: &mut App, - ) -> Task> { - // 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( - &self, - request: R, - session_id: SessionId, - cx: &mut App, - ) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - return self.send_proto_client_request::(request, session_id, cx); - } -} - enum Mode { - Local(LocalMode), - Remote(RemoteConnection), + Building, + Running(LocalMode), } #[derive(Clone)] pub struct LocalMode { client: Arc, - definition: DebugTaskDefinition, binary: DebugAdapterBinary, root_binary: Option>, pub(crate) breakpoint_store: Entity, @@ -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>, worktree: WeakEntity, breakpoint_store: Entity, - config: DebugTaskDefinition, binary: DebugAdapterBinary, messages_tx: futures::channel::mpsc::UnboundedSender, cx: AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let message_handler = Box::new(move |message| { - messages_tx.unbounded_send(message).ok(); - }); + ) -> Result { + 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())?) + 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 { - 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(), - ) + 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> { - 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, 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 for Mode { - fn from(value: RemoteConnection) -> Self { - Self::Remote(value) - } -} impl Mode { fn request_dap( &self, - session_id: SessionId, request: R, cx: &mut Context, ) -> Task> @@ -529,10 +464,13 @@ impl Mode { ::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, - parent_id: Option, + parent_session: Option>, ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, @@ -626,7 +565,8 @@ pub struct Session { is_session_terminated: bool, requests: HashMap>>>>, exception_breakpoints: BTreeMap, - _background_tasks: Vec>, + start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, + background_tasks: Vec>, } 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 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, - worktree: WeakEntity, session_id: SessionId, parent_session: Option>, - binary: DebugAdapterBinary, - config: DebugTaskDefinition, + template: DebugTaskDefinition, start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, - initialized_tx: oneshot::Sender<()>, cx: &mut App, - ) -> Task>> { - let (message_tx, message_rx) = futures::channel::mpsc::unbounded(); + ) -> Entity { + cx.new::(|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(); - cx.spawn(async move |cx| { + 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> { + match &self.mode { + Mode::Building => None, + Mode::Running(local_mode) => local_mode.worktree.upgrade(), + } + } + + pub fn boot( + &mut self, + binary: DebugAdapterBinary, + worktree: Entity, + breakpoint_store: Entity, + dap_store: WeakEntity, + cx: &mut Context, + ) -> Task> { + 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, 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?; + this.update(cx, |this, cx| { + this.mode = Mode::Running(mode); + cx.emit(SessionStateEvent::Running); + })?; - 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, |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 { - self.parent_id + pub fn parent_id(&self, cx: &App) -> Option { + self.parent_session + .as_ref() + .map(|session| session.read(cx).id) + } + + pub fn parent_session(&self) -> Option<&Entity> { + self.parent_session.as_ref() } pub fn capabilities(&self) -> &Capabilities { @@ -821,35 +830,35 @@ impl Session { } pub(crate) fn root_binary(&self) -> Arc { - 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 { - 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) -> Task> { + 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, ) -> Task> { 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, ) { 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, cx: &mut Context, ) -> Task> { - 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::>( &self.capabilities, - self.id, &self.mode, command, process_result, @@ -1199,7 +1214,6 @@ impl Session { fn request_inner( 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, ) -> Task> { - 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(&mut self) { @@ -1569,8 +1576,8 @@ impl Session { pub fn adapter_client(&self) -> Option> { 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, - session_id: SessionId, - parent_session: Option>, - start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, - initialized_tx: oneshot::Sender<()>, - mut message_rx: futures::channel::mpsc::UnboundedReceiver, - mode: LocalMode, - cx: &mut Context, -) -> Session { - let _background_tasks = vec![cx.spawn(async move |this: WeakEntity, 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, - } -} diff --git a/crates/project/src/debugger/test.rs b/crates/project/src/debugger/test.rs index 6c8f422124..d61ec80a67 100644 --- a/crates/project/src/debugger/test.rs +++ b/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) + 'static>( cx: &mut gpui::TestAppContext, configure: T, ) -> Subscription { cx.update(|cx| { - cx.observe_new::(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::(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) + 'static>( - project: &Entity, - cx: &mut gpui::TestAppContext, - config: DebugTaskDefinition, - configure: T, -) -> Task>> { - 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) + 'static>( - project: &Entity, - cx: &mut gpui::TestAppContext, - configure: T, -) -> Task>> { - 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, cx: &mut App) { client.on_request::(move |_, _| Ok(Default::default())); let paths = session diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9a4be5b85e..c27d3bac25 100644 --- a/crates/project/src/project.rs +++ b/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, - ) -> Task>> { - 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, diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 5b5f0b4f72..64f7d4d607 100644 --- a/crates/workspace/src/tasks.rs +++ b/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, + ) { + 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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 06923452dd..b418c3f13e 100644 --- a/crates/workspace/src/workspace.rs +++ b/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>; } +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, on_prompt_for_open_path: Option, terminal_provider: Option>, + debugger_provider: Option>, serializable_items_tx: UnboundedSender>, serialized_ssh_project: Option, _items_serializer: Task>, @@ -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 { self.serialized_ssh_project.clone() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bb7d1d944e..f351d66e36 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -444,7 +444,7 @@ fn initialize_panels( window, async move |workspace: gpui::WeakEntity, 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); })?;