debugger: Allow users to shutdown debug sessions while they're booting (#34362)

This solves problems where users couldn't shut down sessions while
locators or build tasks are running.

I renamed `debugger::Session::Mode` enum to `SessionState` to be more
clear when it's referenced in other crates. I also embedded the boot
task that is created in `SessionState::Building` variant. This allows
sessions to shut down all created threads in their boot process in a
clean and idiomatic way.

Finally, I added a method on terminal that allows killing the active
task.

Release Notes:

- Debugger: Allow shutting down debug sessions while they're booting up
This commit is contained in:
Anthony Eid 2025-07-12 19:16:35 -04:00 committed by GitHub
parent 970a1066f5
commit 8f6b9f0d65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 79 deletions

View file

@ -27,7 +27,7 @@ use text::ToPoint as _;
use itertools::Itertools as _;
use language::Buffer;
use project::debugger::session::{Session, SessionQuirks, SessionStateEvent};
use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent};
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
@ -36,7 +36,7 @@ use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt, maybe};
use util::{ResultExt, debug_panic, maybe};
use workspace::SplitDirection;
use workspace::item::SaveOptions;
use workspace::{
@ -278,22 +278,34 @@ impl DebugPanel {
}
});
cx.spawn(async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {}", error))
.ok();
session.shutdown(cx)
})?
.await;
let boot_task = cx.spawn({
let session = session.clone();
async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {}", error))
.ok();
session.shutdown(cx)
})?
.await;
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
session.update(cx, |session, _| match &mut session.mode {
SessionState::Building(state_task) => {
*state_task = Some(boot_task);
}
SessionState::Running(_) => {
debug_panic!("Session state should be in building because we are just starting it");
}
});
}
pub(crate) fn rerun_last_session(
@ -826,13 +838,24 @@ impl DebugPanel {
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.stop_thread(cx);
if this.session().read(cx).is_building() {
this.session().update(cx, |session, cx| {
session.shutdown(cx).detach()
});
} else {
this.stop_thread(cx);
}
},
))
.disabled(active_session.as_ref().is_none_or(
|session| {
session
.read(cx)
.session(cx)
.read(cx)
.is_terminated()
},
))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip({
let focus_handle = focus_handle.clone();
let label = if capabilities

View file

@ -34,7 +34,7 @@ use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
use project::{
DebugScenarioContext, Project, WorktreeId,
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
terminals::TerminalKind,
};
use rpc::proto::ViewId;
@ -770,6 +770,15 @@ impl RunningState {
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.serialize_layout(window, cx);
}),
cx.subscribe(
&session,
|this, session, event: &SessionStateEvent, cx| match event {
SessionStateEvent::Shutdown if session.read(cx).is_building() => {
this.shutdown(cx);
}
_ => {}
},
),
];
let mut pane_close_subscriptions = HashMap::default();
@ -884,6 +893,7 @@ impl RunningState {
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let is_local = project.read(cx).is_local();
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
adapter,
@ -1599,9 +1609,21 @@ impl RunningState {
})
.log_err();
self.session.update(cx, |session, cx| {
let is_building = self.session.update(cx, |session, cx| {
session.shutdown(cx).detach();
})
matches!(session.mode, session::SessionState::Building(_))
});
if is_building {
self.debug_terminal.update(cx, |terminal, cx| {
if let Some(view) = terminal.terminal.as_ref() {
view.update(cx, |view, cx| {
view.terminal()
.update(cx, |terminal, _| terminal.kill_active_task())
})
}
})
}
}
pub fn stop_thread(&self, cx: &mut Context<Self>) {