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 let current_job = self
.project .project
.read(cx) .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 project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
@ -23,31 +25,40 @@ impl DebugPanel {
let sessions = self.sessions().clone(); let sessions = self.sessions().clone();
let weak = cx.weak_entity(); let weak = cx.weak_entity();
let running_state = running_state.read(cx); 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() active_session.read(cx).session(cx).read(cx).label()
} else { } else {
SharedString::new_static("Unknown Session") SharedString::new_static("Unknown Session")
}; };
let is_terminated = running_state.session().read(cx).is_terminated(); let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = { let is_started = active_session
if is_terminated { .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
Some(Indicator::dot().color(Color::Error))
} else { let session_state_indicator = if is_terminated {
match running_state.thread_status(cx).unwrap_or_default() { Indicator::dot().color(Color::Error).into_any_element()
project::debugger::session::ThreadStatus::Stopped => { } else if !is_started {
Some(Indicator::dot().color(Color::Conflict)) Icon::new(IconName::ArrowCircle)
} .size(IconSize::Small)
_ => Some(Indicator::dot().color(Color::Success)), .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() let trigger = h_flex()
.gap_2() .gap_2()
.when_some(session_state_indicator, |this, indicator| { .child(session_state_indicator)
this.child(indicator)
})
.justify_between() .justify_between()
.child( .child(
DebugPanel::dropdown_label(label) DebugPanel::dropdown_label(label)

View file

@ -110,7 +110,7 @@ impl Console {
} }
fn is_running(&self, cx: &Context<Self>) -> bool { 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( fn handle_stack_frame_list_events(

View file

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