debugger: Separate Continue Program and Continue Thread actions (#63135)
Fixes #63091 Continue Program was sending `singleThread: true` whenever the adapter supported it, which caused some adapters to resume only the selected thread. It now leaves `singleThread` unset and resumes all threads. Adapters that support single-thread execution now show separate Continue Program and Continue Thread buttons. Other adapters still show only Continue Program. <img width="247" height="108" alt="image" src="https://github.com/user-attachments/assets/5059d291-4809-488b-9169-e4817852fb07" /> This change also: - Updates thread states from the continue response instead of waiting for a [continued event, which adapters are not expected to send](https://github.com/microsoft/debug-adapter-protocol/blob/bf8a5d27e8040044b84b863f90916e08925ee811/debugAdapterProtocol.json#L243-L246). - Treats an omitted allThreadsContinued in a continued event as true, as required by the [DAP specification](https://github.com/microsoft/debug-adapter-protocol/blob/main/debugAdapterProtocol.json#L260-L263). Release Notes: - Added a Continue Thread action and fixed Continue Program to resume all threads.
This commit is contained in:
1 parent
3bd76bb912
commit
26b8374358
7 files changed
+286
-37
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5 3l6 5-6 5V3Z"/></svg>
|
||||
|
After Width: | Height: | Size: 189 B |
@@ -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,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Self>) {
|
||||
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<Self>) {
|
||||
let Some(thread_id) = self.thread_id else {
|
||||
return;
|
||||
|
||||
@@ -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::<Continue, _>(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::<dap::requests::Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_single_thread_execution_requests: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
client.on_request::<Continue, _>({
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ pub enum IconName {
|
||||
Debug,
|
||||
DebugBreakpoint,
|
||||
DebugContinue,
|
||||
DebugContinueThread,
|
||||
DebugDetach,
|
||||
DebugDisabledBreakpoint,
|
||||
DebugDisabledLogBreakpoint,
|
||||
|
||||
@@ -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<dap::ContinueResponse>,
|
||||
&mut Context<Self>,
|
||||
) -> Option<dap::ContinueResponse>
|
||||
+ '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>) {
|
||||
self.continue_execution(thread_id, None, cx);
|
||||
}
|
||||
|
||||
pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
|
||||
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<bool>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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::<ContinueCommand>(thread_id),
|
||||
Self::on_continue_response(thread_id),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
||||
Reference in new issue
Block a user