debugger: Always focus the active session whenever it is stopped (#31182)

Closes #ISSUE

Release Notes:

- debugger: Fixed child debug sessions taking precedence over the
parents when spawned.
This commit is contained in:
Piotr Osiewicz 2025-05-22 17:23:31 +02:00 committed by GitHub
parent ee4e43f1b6
commit fa1abd8201
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 131 additions and 64 deletions

View file

@ -273,7 +273,7 @@ impl DebugPanel {
let session = session.clone();
async move |this, cx| {
let debug_session =
Self::register_session(this.clone(), session.clone(), cx).await?;
Self::register_session(this.clone(), session.clone(), true, cx).await?;
let definition = debug_session
.update_in(cx, |debug_session, window, cx| {
debug_session.running_state().update(cx, |running, cx| {
@ -318,69 +318,21 @@ impl DebugPanel {
pub(crate) async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
focus: bool,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let debug_session = register_session_inner(&this, session, cx).await?;
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let workspace = this.update_in(cx, |this, window, cx| {
if focus {
this.activate_session(debug_session.clone(), window, cx);
}
let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// 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(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
this.activate_session(debug_session.clone(), window, cx);
(debug_session, this.workspace.clone())
this.workspace.clone()
})?;
workspace.update_in(cx, |workspace, window, cx| {
workspace.focus_panel::<Self>(window, cx);
})?;
Ok(debug_session)
}
@ -418,7 +370,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this.clone(), session, true, cx).await?;
task.await
})
.detach_and_log_err(cx);
@ -441,7 +393,6 @@ impl DebugPanel {
let adapter = parent_session.read(cx).adapter().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 =
@ -452,7 +403,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this, session, false, cx).await?;
task.await
})
.detach_and_log_err(cx);
@ -910,6 +861,21 @@ impl DebugPanel {
}
}
pub(crate) fn activate_session_by_id(
&mut self,
session_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(session) = self
.sessions
.iter()
.find(|session| session.read(cx).session_id(cx) == session_id)
{
self.activate_session(session.clone(), window, cx);
}
}
pub(crate) fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
@ -923,7 +889,7 @@ impl DebugPanel {
this.go_to_selected_stack_frame(window, cx);
});
});
self.active_session = Some(session_item.clone());
self.active_session = Some(session_item);
cx.notify();
}
@ -999,6 +965,67 @@ impl DebugPanel {
}
}
async fn register_session_inner(
this: &WeakEntity<DebugPanel>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// 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(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
debug_session
})?;
Ok(debug_session)
}
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}

View file

@ -585,15 +585,26 @@ impl RunningState {
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
match event {
SessionEvent::Stopped(thread_id) => {
this.workspace
let panel = this
.workspace
.update(cx, |workspace, cx| {
workspace.open_panel::<crate::DebugPanel>(window, cx);
workspace.panel::<crate::DebugPanel>(cx)
})
.log_err();
.log_err()
.flatten();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, window, cx);
}
if let Some(panel) = panel {
let id = this.session_id;
window.defer(cx, move |window, cx| {
panel.update(cx, |this, cx| {
this.activate_session_by_id(id, window, cx);
})
})
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));

View file

@ -423,6 +423,13 @@ async fn test_handle_start_debugging_request(
}
});
let sessions = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel.read(cx).sessions()
})
.unwrap();
assert_eq!(sessions.len(), 1);
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
@ -435,20 +442,42 @@ async fn test_handle_start_debugging_request(
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
// Active session does not change on spawn.
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, sessions[0].read(cx).session(cx));
assert!(active_session.read(cx).parent_session().is_none());
let current_sessions = debug_panel.read(cx).sessions();
assert_eq!(current_sessions.len(), 2);
assert_eq!(current_sessions[0], sessions[0]);
let parent_session = current_sessions[1]
.read(cx)
.session(cx)
.read(cx)
.parent_session()
.unwrap();
assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
// We should preserve the original binary (params to spawn process etc.) except for launch params
// (as they come from reverse spawn request).
let mut original_binary = parent_session.read(cx).binary().clone();
original_binary.request_args = StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
configuration: fake_config.clone(),
};
assert_eq!(active_session.read(cx).binary(), &original_binary);
assert_eq!(
current_sessions[1].read(cx).session(cx).read(cx).binary(),
&original_binary
);
})
.unwrap();