debugger: Add spinners while session is starting up (#31548)

Release Notes:

- Debugger Beta: Added a spinner to the debug panel when a session is
starting up.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Julia <julia@zed.dev>
This commit is contained in:
Cole Miller 2025-05-28 21:58:40 -04:00 committed by GitHub
parent 384b11392a
commit f9407db7d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 33 deletions

View file

@ -311,6 +311,31 @@ impl ActivityIndicator {
});
}
if let Some(session) = self
.project
.read(cx)
.dap_store()
.read(cx)
.sessions()
.find(|s| !s.read(cx).is_started())
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: Some(session.read(cx).label().to_string()),
on_click: None,
});
}
let current_job = self
.project
.read(cx)

View file

@ -1,4 +1,6 @@
use gpui::Entity;
use std::time::Duration;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
@ -23,31 +25,40 @@ impl DebugPanel {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
let label = if let Some(active_session) = active_session.clone() {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => {
Indicator::dot().color(Color::Conflict).into_any_element()
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(label)

View file

@ -110,7 +110,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
self.session.read(cx).is_running()
}
fn handle_stack_frame_list_events(

View file

@ -121,16 +121,17 @@ impl From<dap::Thread> for Thread {
pub enum Mode {
Building,
Running(LocalMode),
Running(RunningMode),
}
#[derive(Clone)]
pub struct LocalMode {
pub struct RunningMode {
client: Arc<DebugAdapterClient>,
binary: DebugAdapterBinary,
tmp_breakpoint: Option<SourceBreakpoint>,
worktree: WeakEntity<Worktree>,
executor: BackgroundExecutor,
is_started: bool,
}
fn client_source(abs_path: &Path) -> dap::Source {
@ -148,7 +149,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
}
}
impl LocalMode {
impl RunningMode {
async fn new(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
@ -181,6 +182,7 @@ impl LocalMode {
tmp_breakpoint: None,
binary,
executor: cx.background_executor().clone(),
is_started: false,
})
}
@ -373,7 +375,7 @@ impl LocalMode {
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &App,
cx: &mut Context<Session>,
) -> Task<Result<()>> {
let raw = self.binary.request_args.clone();
@ -405,7 +407,7 @@ impl LocalMode {
let this = self.clone();
let worktree = self.worktree().clone();
let configuration_sequence = cx.spawn({
async move |cx| {
async move |_, cx| {
let breakpoint_store =
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
initialized_rx.await?;
@ -453,9 +455,20 @@ impl LocalMode {
}
});
cx.background_spawn(async move {
futures::future::try_join(launch, configuration_sequence).await?;
Ok(())
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
cx.spawn(async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
if let Some(this) = this.as_running_mut() {
this.is_started = true;
cx.notify();
}
})
.ok();
anyhow::Ok(())
})
}
@ -704,7 +717,7 @@ impl Session {
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_local_mut())
.then(|| this.as_running_mut())
.flatten()
{
local
@ -714,7 +727,7 @@ impl Session {
}
BreakpointStoreEvent::BreakpointsCleared(paths) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_local_mut())
.then(|| this.as_running_mut())
.flatten()
{
local.unset_breakpoints_from_paths(paths, cx).detach();
@ -806,7 +819,7 @@ impl Session {
let parent_session = self.parent_session.clone();
cx.spawn(async move |this, cx| {
let mode = LocalMode::new(
let mode = RunningMode::new(
id,
parent_session,
worktree.downgrade(),
@ -906,18 +919,29 @@ impl Session {
return tx;
}
pub fn is_local(&self) -> bool {
pub fn is_started(&self) -> bool {
match &self.mode {
Mode::Building => false,
Mode::Running(running) => running.is_started,
}
}
pub fn is_building(&self) -> bool {
matches!(self.mode, Mode::Building)
}
pub fn is_running(&self) -> bool {
matches!(self.mode, Mode::Running(_))
}
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
match &mut self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
}
}
pub fn as_local(&self) -> Option<&LocalMode> {
pub fn as_running(&self) -> Option<&RunningMode> {
match &self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
@ -1140,7 +1164,7 @@ impl Session {
body: Option<serde_json::Value>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(local_session) = self.as_local() else {
let Some(local_session) = self.as_running() else {
unreachable!("Cannot respond to remote client");
};
let client = local_session.client.clone();
@ -1162,7 +1186,7 @@ impl Session {
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
// todo(debugger): Find a clean way to get around the clone
let breakpoint_store = self.breakpoint_store.clone();
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
let breakpoint = local.tmp_breakpoint.take()?;
let path = breakpoint.path.clone();
Some((local, path))
@ -1528,7 +1552,7 @@ impl Session {
self.ignore_breakpoints = ignore;
if let Some(local) = self.as_local() {
if let Some(local) = self.as_running() {
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
} else {
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
@ -1550,7 +1574,7 @@ impl Session {
}
fn send_exception_breakpoints(&mut self, cx: &App) {
if let Some(local) = self.as_local() {
if let Some(local) = self.as_running() {
let exception_filters = self
.exception_breakpoints
.values()