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

Cole Miller , Remco Smits , and Julia created

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>

Change summary

crates/activity_indicator/src/activity_indicator.rs | 25 ++++++
crates/debugger_ui/src/dropdown_menus.rs            | 39 ++++++---
crates/debugger_ui/src/session/running/console.rs   |  2 
crates/project/src/debugger/session.rs              | 60 ++++++++++----
4 files changed, 93 insertions(+), 33 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -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)

crates/debugger_ui/src/dropdown_menus.rs 🔗

@@ -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)

crates/project/src/debugger/session.rs 🔗

@@ -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()