debugger: Add support for label presentation hints for stack frames (#32719)

Anthony Eid and Remco Smits created

Release Notes:

- debugger: Add support for `Label` stack frame kinds

Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

crates/debugger_ui/src/session/running/stack_frame_list.rs | 66 ++++++-
crates/debugger_ui/src/stack_trace_view.rs                 |  4 
crates/debugger_ui/src/tests/variable_list.rs              | 25 ++
3 files changed, 72 insertions(+), 23 deletions(-)

Detailed changes

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

@@ -5,9 +5,10 @@ use std::time::Duration;
 use anyhow::{Context as _, Result, anyhow};
 use dap::StackFrameId;
 use gpui::{
-    AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
-    Subscription, Task, WeakEntity, list,
+    AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
+    Stateful, Subscription, Task, WeakEntity, list,
 };
+use util::debug_panic;
 
 use crate::StackTraceView;
 use language::PointUtf16;
@@ -43,6 +44,8 @@ pub struct StackFrameList {
 #[derive(Debug, PartialEq, Eq)]
 pub enum StackFrameEntry {
     Normal(dap::StackFrame),
+    /// Used to indicate that the frame is artificial and is a visual label or separator
+    Label(dap::StackFrame),
     Collapsed(Vec<dap::StackFrame>),
 }
 
@@ -99,18 +102,18 @@ impl StackFrameList {
         &self.entries
     }
 
-    pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {
+    pub(crate) fn flatten_entries(
+        &self,
+        show_collapsed: bool,
+        show_labels: bool,
+    ) -> Vec<dap::StackFrame> {
         self.entries
             .iter()
             .flat_map(|frame| match frame {
                 StackFrameEntry::Normal(frame) => vec![frame.clone()],
-                StackFrameEntry::Collapsed(frames) => {
-                    if show_collapsed {
-                        frames.clone()
-                    } else {
-                        vec![]
-                    }
-                }
+                StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
+                StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
+                _ => vec![],
             })
             .collect::<Vec<_>>()
     }
@@ -176,9 +179,7 @@ impl StackFrameList {
             .and_then(|ix| self.entries.get(ix))
             .and_then(|entry| match entry {
                 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
-                StackFrameEntry::Collapsed(stack_frames) => {
-                    stack_frames.first().map(|stack_frame| stack_frame.id)
-                }
+                StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
             });
         let mut entries = Vec::new();
         let mut collapsed_entries = Vec::new();
@@ -202,6 +203,9 @@ impl StackFrameList {
                 Some(dap::StackFramePresentationHint::Deemphasize) => {
                     collapsed_entries.push(stack_frame.dap.clone());
                 }
+                Some(dap::StackFramePresentationHint::Label) => {
+                    entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
+                }
                 _ => {
                     let collapsed_entries = std::mem::take(&mut collapsed_entries);
                     if !collapsed_entries.is_empty() {
@@ -234,9 +238,7 @@ impl StackFrameList {
         } else if let Some(old_selected_frame_id) = old_selected_frame_id {
             let ix = self.entries.iter().position(|entry| match entry {
                 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
-                StackFrameEntry::Collapsed(frames) => {
-                    frames.iter().any(|frame| frame.id == old_selected_frame_id)
-                }
+                StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
             });
             self.selected_ix = ix;
         }
@@ -256,6 +258,7 @@ impl StackFrameList {
             .entries
             .iter()
             .flat_map(|entry| match entry {
+                StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
                 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
                 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
             })
@@ -380,6 +383,33 @@ impl StackFrameList {
         });
     }
 
+    fn render_label_entry(
+        &self,
+        stack_frame: &dap::StackFrame,
+        _cx: &mut Context<Self>,
+    ) -> AnyElement {
+        h_flex()
+            .rounded_md()
+            .justify_between()
+            .w_full()
+            .group("")
+            .id(("label-stack-frame", stack_frame.id))
+            .p_1()
+            .on_any_mouse_down(|_, _, cx| {
+                cx.stop_propagation();
+            })
+            .child(
+                v_flex().justify_center().gap_0p5().child(
+                    Label::new(stack_frame.name.clone())
+                        .size(LabelSize::Small)
+                        .weight(FontWeight::BOLD)
+                        .truncate()
+                        .color(Color::Info),
+                ),
+            )
+            .into_any()
+    }
+
     fn render_normal_entry(
         &self,
         ix: usize,
@@ -541,6 +571,7 @@ impl StackFrameList {
 
     fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
         match &self.entries[ix] {
+            StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
             StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
             StackFrameEntry::Collapsed(stack_frames) => {
                 self.render_collapsed_entry(ix, stack_frames, cx)
@@ -657,6 +688,9 @@ impl StackFrameList {
                 self.go_to_stack_frame_inner(stack_frame, window, cx)
                     .detach_and_log_err(cx)
             }
+            StackFrameEntry::Label(_) => {
+                debug_panic!("You should not be able to select a label stack frame")
+            }
             StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),
         }
         cx.notify();

crates/debugger_ui/src/stack_trace_view.rs 🔗

@@ -148,7 +148,7 @@ impl StackTraceView {
 
         let stack_frames = self
             .stack_frame_list
-            .read_with(cx, |list, _| list.flatten_entries(false));
+            .read_with(cx, |list, _| list.flatten_entries(false, false));
 
         let frames_to_open: Vec<_> = stack_frames
             .into_iter()
@@ -237,7 +237,7 @@ impl StackTraceView {
 
         let stack_frames = self
             .stack_frame_list
-            .read_with(cx, |session, _| session.flatten_entries(false));
+            .read_with(cx, |session, _| session.flatten_entries(false, false));
 
         let active_idx = self
             .selected_stack_frame_id

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

@@ -191,7 +191,10 @@ async fn test_basic_fetch_initial_scope_and_variables(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(true), list.opened_stack_frame_id())
+                (
+                    list.flatten_entries(true, true),
+                    list.opened_stack_frame_id(),
+                )
             });
 
         assert_eq!(stack_frames, stack_frame_list);
@@ -432,7 +435,10 @@ async fn test_fetch_variables_for_multiple_scopes(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(true), list.opened_stack_frame_id())
+                (
+                    list.flatten_entries(true, true),
+                    list.opened_stack_frame_id(),
+                )
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1459,7 +1465,10 @@ async fn test_variable_list_only_sends_requests_when_rendering(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(true), list.opened_stack_frame_id())
+                (
+                    list.flatten_entries(true, true),
+                    list.opened_stack_frame_id(),
+                )
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1741,7 +1750,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(true), list.opened_stack_frame_id())
+                (
+                    list.flatten_entries(true, true),
+                    list.opened_stack_frame_id(),
+                )
             });
 
         let variable_list = running_state.variable_list().read(cx);
@@ -1796,7 +1808,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(true), list.opened_stack_frame_id())
+                (
+                    list.flatten_entries(true, true),
+                    list.opened_stack_frame_id(),
+                )
             });
 
         let variable_list = running_state.variable_list().read(cx);