diff --git a/assets/icons/debug_continue_thread.svg b/assets/icons/debug_continue_thread.svg new file mode 100644 index 0000000000..2db1c54035 --- /dev/null +++ b/assets/icons/debug_continue_thread.svg @@ -0,0 +1 @@ + diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0a9147fb63..a55c07f528 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -4,10 +4,11 @@ use crate::session::running::RunningState; use crate::session::running::breakpoint_list::BreakpointList; use crate::{ - ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList, - FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, - NewProcessModal, NewProcessMode, Pause, RerunSession, StepInto, StepOut, StepOver, Stop, - ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, + ClearAllBreakpoints, Continue, ContinueThread, CopyDebugAdapterArguments, Detach, + FocusBreakpointList, FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, + FocusTerminal, FocusVariables, NewProcessModal, NewProcessMode, Pause, RerunSession, StepInto, + StepOut, StepOver, Stop, ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, + persistence, spawn_task_or_modal, }; use anyhow::{Context as _, Result, anyhow}; use collections::IndexMap; @@ -726,28 +727,63 @@ impl DebugPanel { }), ) } else { - this.child( - IconButton::new( - "debug-continue", - IconName::DebugContinue, - ) - .icon_size(IconSize::Small) - .disabled(thread_status != ThreadStatus::Stopped) - .on_click(window.listener_for( - running_state, - |this, _, _window, cx| this.continue_thread(cx), - )) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Continue Program", - &Continue, - &focus_handle, - cx, + let continue_button = IconButton::new( + "debug-continue", + IconName::DebugContinue, + ) + .icon_size(IconSize::Small) + .disabled(thread_status != ThreadStatus::Stopped) + .on_click(window.listener_for( + running_state, + |this, _, _window, cx| { + this.continue_program(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |_window, cx| { + Tooltip::for_action_in( + "Continue Program", + &Continue, + &focus_handle, + cx, + ) + } + }); + + this.child(continue_button).when( + capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(), + |this| { + this.child( + IconButton::new( + "debug-continue-thread", + IconName::DebugContinueThread, ) - } - }), + .icon_size(IconSize::Small) + .disabled( + thread_status != ThreadStatus::Stopped, + ) + .on_click(window.listener_for( + running_state, + |this, _, _window, cx| { + this.continue_thread(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |_window, cx| { + Tooltip::for_action_in( + "Continue Thread", + &ContinueThread, + &focus_handle, + cx, + ) + } + }), + ) + }, ) } }) diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 2fe87d1ef0..35096898de 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -31,8 +31,10 @@ actions!( [ /// Starts a new debugging session. Start, - /// Continues execution until the next breakpoint. + /// Continues all threads until the next breakpoint. Continue, + /// Continues the selected thread until the next breakpoint. + ContinueThread, /// Detaches the debugger from the running process. Detach, /// Pauses the currently running program. @@ -159,6 +161,9 @@ pub fn init(cx: &mut App) { let caps = running_state.capabilities(cx); let supports_step_back = caps.supports_step_back.unwrap_or_default(); + let supports_single_thread_execution_requests = caps + .supports_single_thread_execution_requests + .unwrap_or_default(); let supports_detach = running_state.session().read(cx).is_attached(); let status = running_state.thread_status(cx); @@ -200,10 +205,18 @@ pub fn init(cx: &mut App) { let active_item = active_item.clone(); move |_: &Continue, _, cx| { active_item - .update(cx, |item, cx| item.continue_thread(cx)) + .update(cx, |item, cx| item.continue_program(cx)) .ok(); } }) + .when(supports_single_thread_execution_requests, |div| { + let active_item = active_item.clone(); + div.on_action(move |_: &ContinueThread, _, cx| { + active_item + .update(cx, |item, cx| item.continue_thread(cx)) + .ok(); + }) + }) }) .when(supports_detach, |div| { let active_item = active_item.clone(); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index f0e1b471f1..8f4301913d 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1746,6 +1746,16 @@ impl RunningState { .update(cx, |list, cx| list.schedule_refresh(true, window, cx)); } + pub fn continue_program(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + + self.session().update(cx, |state, cx| { + state.continue_program(thread_id, cx); + }); + } + pub fn continue_thread(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 90b36074cc..ddc2bfa86e 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -891,6 +891,142 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown( }); } +#[gpui::test] +async fn test_continue_response_updates_thread_statuses( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + fs.insert_tree(path!("/project"), json!({ "main.rs": "" })) + .await; + + 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 = start_debug_session(&workspace, cx, |client| { + client.on_request::(move |_, args| { + assert_eq!(args.single_thread, None); + Ok(dap::ContinueResponse { + all_threads_continued: None, + }) + }); + }) + .unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .await; + cx.run_until_parked(); + + session.update(cx, |session, cx| { + assert_eq!(session.thread_status(ThreadId(1)), ThreadStatus::Stopped); + assert_eq!(session.thread_status(ThreadId(2)), ThreadStatus::Stopped); + session.continue_program(ThreadId(1), cx); + }); + cx.run_until_parked(); + + session.update(cx, |session, _| { + assert_eq!(session.thread_status(ThreadId(1)), ThreadStatus::Running); + assert_eq!(session.thread_status(ThreadId(2)), ThreadStatus::Running); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .await; + cx.run_until_parked(); + + cx.dispatch_action(ContinueThread); + cx.run_until_parked(); + + session.update(cx, |session, _| { + assert_eq!(session.thread_status(ThreadId(1)), ThreadStatus::Stopped); + assert_eq!(session.thread_status(ThreadId(2)), ThreadStatus::Stopped); + }); +} + +#[gpui::test] +async fn test_continue_thread_action_only_resumes_selected_thread( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + fs.insert_tree(path!("/project"), json!({ "main.rs": "" })) + .await; + + 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 continue_requested = Arc::new(AtomicBool::new(false)); + + let session = start_debug_session(&workspace, cx, { + let continue_requested = continue_requested.clone(); + move |client| { + client.on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_single_thread_execution_requests: Some(true), + ..Default::default() + }) + }); + client.on_request::({ + let continue_requested = continue_requested.clone(); + move |_, args| { + assert_eq!(args.single_thread, Some(true)); + continue_requested.store(true, Ordering::SeqCst); + Ok(dap::ContinueResponse { + all_threads_continued: Some(false), + }) + } + }); + } + }) + .unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .await; + cx.run_until_parked(); + + cx.dispatch_action(ContinueThread); + cx.run_until_parked(); + + assert!(continue_requested.load(Ordering::SeqCst)); + session.update(cx, |session, _| { + assert_eq!(session.thread_status(ThreadId(1)), ThreadStatus::Running); + assert_eq!(session.thread_status(ThreadId(2)), ThreadStatus::Stopped); + }); +} + #[gpui::test] async fn test_debug_panel_item_thread_status_reset_on_failure( executor: BackgroundExecutor, @@ -1035,14 +1171,14 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( for operation in &[ "step_over", - "continue_thread", + "continue_program", "step_back", "step_in", "step_out", ] { running_state.update(cx, |running_state, cx| match *operation { "step_over" => running_state.step_over(cx), - "continue_thread" => running_state.continue_thread(cx), + "continue_program" => running_state.continue_program(cx), "step_back" => running_state.step_back(cx), "step_in" => running_state.step_in(cx), "step_out" => running_state.step_out(cx), @@ -1056,7 +1192,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( .thread_status(cx) .expect("There should be an active thread selected"), match *operation { - "continue_thread" => ThreadStatus::Running, + "continue_program" => ThreadStatus::Running, _ => ThreadStatus::Stepping, }, "Thread status was not set to correct intermediate state after {} request", @@ -1079,7 +1215,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( // update state to running, so we can test it actually changes the status back to stopped running_state .session() - .update(cx, |session, cx| session.continue_thread(thread_id, cx)); + .update(cx, |session, cx| session.continue_program(thread_id, cx)); }); } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 1c9966a1e7..1b2eda162c 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -91,6 +91,7 @@ pub enum IconName { Debug, DebugBreakpoint, DebugContinue, + DebugContinueThread, DebugDetach, DebugDisabledBreakpoint, DebugDisabledLogBreakpoint, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 56c5681aec..6aa115d607 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1357,7 +1357,7 @@ impl Session { cx.spawn(async move |this, cx| { task.await; this.update(cx, |this, cx| { - this.continue_thread(active_thread_id, cx); + this.continue_program(active_thread_id, cx); }) }) .detach(); @@ -1526,7 +1526,8 @@ impl Session { } Events::Stopped(event) => self.handle_stopped_event(event, cx), Events::Continued(event) => { - if event.all_threads_continued.unwrap_or_default() { + // DAP defines an omitted `allThreadsContinued` as `true`. + if event.all_threads_continued.unwrap_or(true) { self.active_snapshot.thread_states.continue_all_threads(); self.breakpoint_store.update(cx, |store, cx| { store.remove_active_position(Some(self.session_id()), cx) @@ -2141,6 +2142,38 @@ impl Session { } } + fn on_continue_response( + thread_id: ThreadId, + ) -> impl FnOnce( + &mut Self, + Result, + &mut Context, + ) -> Option + + 'static { + move |this, response, cx| match response.log_err() { + Some(response) => { + if response.all_threads_continued.unwrap_or(true) { + this.active_snapshot.thread_states.continue_all_threads(); + } else { + this.active_snapshot + .thread_states + .continue_thread(thread_id); + } + this.breakpoint_store.update(cx, |store, cx| { + store.remove_active_position(Some(this.session_id()), cx) + }); + this.invalidate_generic(); + cx.notify(); + Some(response) + } + None => { + this.active_snapshot.thread_states.stop_thread(thread_id); + cx.notify(); + None + } + } + } + fn clear_active_debug_line_response( &mut self, response: Result<()>, @@ -2279,11 +2312,30 @@ impl Session { }) } + pub fn continue_program(&mut self, thread_id: ThreadId, cx: &mut Context) { + self.continue_execution(thread_id, None, cx); + } + pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { + if !self + .capabilities + .supports_single_thread_execution_requests + .unwrap_or_default() + { + return; + } + + self.continue_execution(thread_id, Some(true), cx); + } + + fn continue_execution( + &mut self, + thread_id: ThreadId, + single_thread: Option, + cx: &mut Context, + ) { self.select_historic_snapshot(None, cx); - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; self.active_snapshot .thread_states .continue_thread(thread_id); @@ -2291,10 +2343,10 @@ impl Session { ContinueCommand { args: ContinueArguments { thread_id: thread_id.0, - single_thread: supports_single_thread_execution_requests, + single_thread, }, }, - Self::on_step_response::(thread_id), + Self::on_continue_response(thread_id), cx, ) .detach();