debugger: Fix stack frame list flickering (#29282)

Cole Miller and Anthony Eid created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

crates/debugger_ui/src/session/running.rs                  | 16 +-
crates/debugger_ui/src/session/running/stack_frame_list.rs | 58 +++++--
crates/debugger_ui/src/tests/stack_frame_list.rs           |  9 
crates/project/src/debugger/session.rs                     |  1 
4 files changed, 55 insertions(+), 29 deletions(-)

Detailed changes

crates/debugger_ui/src/session/running.rs 🔗

@@ -411,12 +411,12 @@ impl RunningState {
                             .log_err();
 
                         if let Some(thread_id) = thread_id {
-                            this.select_thread(*thread_id, cx);
+                            this.select_thread(*thread_id, window, cx);
                         }
                     }
                     SessionEvent::Threads => {
                         let threads = this.session.update(cx, |this, cx| this.threads(cx));
-                        this.select_current_thread(&threads, cx);
+                        this.select_current_thread(&threads, window, cx);
                     }
                     SessionEvent::CapabilitiesLoaded => {
                         let capabilities = this.capabilities(cx);
@@ -731,6 +731,7 @@ impl RunningState {
     pub fn select_current_thread(
         &mut self,
         threads: &Vec<(Thread, ThreadStatus)>,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let selected_thread = self
@@ -743,7 +744,7 @@ impl RunningState {
         };
 
         if Some(ThreadId(selected_thread.id)) != self.thread_id {
-            self.select_thread(ThreadId(selected_thread.id), cx);
+            self.select_thread(ThreadId(selected_thread.id), window, cx);
         }
     }
 
@@ -756,7 +757,7 @@ impl RunningState {
             .map(|id| self.session().read(cx).thread_status(id))
     }
 
-    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
+    fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
         if self.thread_id.is_some_and(|id| id == thread_id) {
             return;
         }
@@ -764,8 +765,7 @@ impl RunningState {
         self.thread_id = Some(thread_id);
 
         self.stack_frame_list
-            .update(cx, |list, cx| list.refresh(cx));
-        cx.notify();
+            .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
     }
 
     pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
@@ -917,9 +917,9 @@ impl RunningState {
                 for (thread, _) in threads {
                     let state = state.clone();
                     let thread_id = thread.id;
-                    this = this.entry(thread.name, None, move |_, cx| {
+                    this = this.entry(thread.name, None, move |window, cx| {
                         state.update(cx, |state, cx| {
-                            state.select_thread(ThreadId(thread_id), cx);
+                            state.select_thread(ThreadId(thread_id), window, cx);
                         });
                     });
                 }

crates/debugger_ui/src/session/running/stack_frame_list.rs 🔗

@@ -1,5 +1,6 @@
 use std::path::Path;
 use std::sync::Arc;
+use std::time::Duration;
 
 use anyhow::{Result, anyhow};
 use dap::StackFrameId;
@@ -28,11 +29,11 @@ pub struct StackFrameList {
     _subscription: Subscription,
     session: Entity<Session>,
     state: WeakEntity<RunningState>,
-    invalidate: bool,
     entries: Vec<StackFrameEntry>,
     workspace: WeakEntity<Workspace>,
     selected_stack_frame_id: Option<StackFrameId>,
     scrollbar_state: ScrollbarState,
+    _refresh_task: Task<()>,
 }
 
 #[allow(clippy::large_enum_variant)]
@@ -68,14 +69,17 @@ impl StackFrameList {
         );
 
         let _subscription =
-            cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
-                SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
-                    this.refresh(cx);
+            cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
+                SessionEvent::Threads => {
+                    this.schedule_refresh(false, window, cx);
+                }
+                SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
+                    this.schedule_refresh(true, window, cx);
                 }
                 _ => {}
             });
 
-        Self {
+        let mut this = Self {
             scrollbar_state: ScrollbarState::new(list.clone()),
             list,
             session,
@@ -83,10 +87,12 @@ impl StackFrameList {
             focus_handle,
             state,
             _subscription,
-            invalidate: true,
             entries: Default::default(),
             selected_stack_frame_id: None,
-        }
+            _refresh_task: Task::ready(()),
+        };
+        this.schedule_refresh(true, window, cx);
+        this
     }
 
     #[cfg(test)]
@@ -136,10 +142,32 @@ impl StackFrameList {
         self.selected_stack_frame_id
     }
 
-    pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
-        self.invalidate = true;
-        self.entries.clear();
-        cx.notify();
+    pub(super) fn schedule_refresh(
+        &mut self,
+        select_first: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
+
+        self._refresh_task = cx.spawn_in(window, async move |this, cx| {
+            let debounce = this
+                .update(cx, |this, cx| {
+                    let new_stack_frames = this.stack_frames(cx);
+                    new_stack_frames.is_empty() && !this.entries.is_empty()
+                })
+                .ok()
+                .unwrap_or_default();
+
+            if debounce {
+                cx.background_executor().timer(REFRESH_DEBOUNCE).await;
+            }
+            this.update_in(cx, |this, window, cx| {
+                this.build_entries(select_first, window, cx);
+                cx.notify();
+            })
+            .ok();
+        })
     }
 
     pub fn build_entries(
@@ -515,13 +543,7 @@ impl StackFrameList {
 }
 
 impl Render for StackFrameList {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        if self.invalidate {
-            self.build_entries(self.entries.is_empty(), window, cx);
-            self.invalidate = false;
-            cx.notify();
-        }
-
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         div()
             .size_full()
             .p_1()

crates/debugger_ui/src/tests/stack_frame_list.rs 🔗

@@ -152,7 +152,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
     cx.run_until_parked();
 
     // select first thread
-    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
+    active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
         session
             .mode()
             .as_running()
@@ -162,6 +162,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
                     &running_state
                         .session()
                         .update(cx, |session, cx| session.threads(cx)),
+                    window,
                     cx,
                 );
             });
@@ -330,7 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
     cx.run_until_parked();
 
     // select first thread
-    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
+    active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
         session
             .mode()
             .as_running()
@@ -340,6 +341,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
                     &running_state
                         .session()
                         .update(cx, |session, cx| session.threads(cx)),
+                    window,
                     cx,
                 );
             });
@@ -704,7 +706,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
     cx.run_until_parked();
 
     // select first thread
-    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
+    active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
         session
             .mode()
             .as_running()
@@ -714,6 +716,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
                     &running_state
                         .session()
                         .update(cx, |session, cx| session.threads(cx)),
+                    window,
                     cx,
                 );
             });