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();