debugger: Add actions and keybindings for opening the thread and session menus (#31135)

Cole Miller created

Makes it possible to open and navigate these menus from the keyboard.

I also removed the eager previewing behavior for the thread picker,
which was buggy and came with a jarring layout shift.

Release Notes:

- Debugger Beta: Added the `debugger: open thread picker` and `debugger:
open session picker` actions.

Change summary

assets/keymaps/default-linux.json         |  7 +++
assets/keymaps/default-macos.json         |  7 +++
crates/debugger_ui/src/debugger_panel.rs  | 36 ++++++++++++++++
crates/debugger_ui/src/debugger_ui.rs     |  2 
crates/debugger_ui/src/dropdown_menus.rs  |  8 ++-
crates/debugger_ui/src/session/running.rs |  4 
crates/ui/src/components/context_menu.rs  | 53 +-----------------------
crates/ui/src/components/dropdown_menu.rs | 27 +++++++-----
8 files changed, 76 insertions(+), 68 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -862,6 +862,13 @@
       "alt-l": "git::GenerateCommitMessage"
     }
   },
+  {
+    "context": "DebugPanel",
+    "bindings": {
+      "ctrl-t": "debugger::ToggleThreadPicker",
+      "ctrl-i": "debugger::ToggleSessionPicker"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -929,6 +929,13 @@
       "alt-tab": "git::GenerateCommitMessage"
     }
   },
+  {
+    "context": "DebugPanel",
+    "bindings": {
+      "cmd-t": "debugger::ToggleThreadPicker",
+      "cmd-i": "debugger::ToggleSessionPicker"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "use_key_equivalents": true,

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
     FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
     ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
-    persistence,
+    ToggleSessionPicker, ToggleThreadPicker, persistence,
 };
 use anyhow::{Context as _, Result, anyhow};
 use command_palette_hooks::CommandPaletteFilter;
@@ -31,7 +31,7 @@ use settings::Settings;
 use std::any::TypeId;
 use std::sync::Arc;
 use task::{DebugScenario, TaskContext};
-use ui::{ContextMenu, Divider, Tooltip, prelude::*};
+use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
 use workspace::SplitDirection;
 use workspace::{
     Pane, Workspace,
@@ -65,6 +65,8 @@ pub struct DebugPanel {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
+    pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
+    pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
     fs: Arc<dyn Fs>,
 }
 
@@ -77,6 +79,8 @@ impl DebugPanel {
         cx.new(|cx| {
             let project = workspace.project().clone();
             let focus_handle = cx.focus_handle();
+            let thread_picker_menu_handle = PopoverMenuHandle::default();
+            let session_picker_menu_handle = PopoverMenuHandle::default();
 
             let debug_panel = Self {
                 size: px(300.),
@@ -87,6 +91,8 @@ impl DebugPanel {
                 workspace: workspace.weak_handle(),
                 context_menu: None,
                 fs: workspace.app_state().fs.clone(),
+                thread_picker_menu_handle,
+                session_picker_menu_handle,
             };
 
             debug_panel
@@ -1033,6 +1039,14 @@ impl DebugPanel {
             })
             .unwrap_or_else(|err| Task::ready(Err(err)))
     }
+
+    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.thread_picker_menu_handle.toggle(window, cx);
+    }
+
+    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.session_picker_menu_handle.toggle(window, cx);
+    }
 }
 
 impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -1249,6 +1263,24 @@ impl Render for DebugPanel {
                     .ok();
                 }
             })
+            .on_action({
+                let this = this.clone();
+                move |_: &ToggleThreadPicker, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.toggle_thread_picker(window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &ToggleSessionPicker, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.toggle_session_picker(window, cx);
+                    })
+                    .ok();
+                }
+            })
             .when(self.active_session.is_some(), |this| {
                 this.on_mouse_down(
                     MouseButton::Right,

crates/debugger_ui/src/dropdown_menus.rs 🔗

@@ -132,7 +132,8 @@ impl DebugPanel {
                         this
                     }),
                 )
-                .style(DropdownStyle::Ghost),
+                .style(DropdownStyle::Ghost)
+                .handle(self.session_picker_menu_handle.clone()),
             )
         } else {
             None
@@ -163,7 +164,7 @@ impl DebugPanel {
                 DropdownMenu::new_with_element(
                     ("thread-list", session_id.0),
                     trigger,
-                    ContextMenu::build_eager(window, cx, move |mut this, _, _| {
+                    ContextMenu::build(window, cx, move |mut this, _, _| {
                         for (thread, _) in threads {
                             let running_state = running_state.clone();
                             let thread_id = thread.id;
@@ -177,7 +178,8 @@ impl DebugPanel {
                     }),
                 )
                 .disabled(session_terminated)
-                .style(DropdownStyle::Ghost),
+                .style(DropdownStyle::Ghost)
+                .handle(self.thread_picker_menu_handle.clone()),
             )
         } else {
             None

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

@@ -96,7 +96,7 @@ impl Render for RunningState {
             .find(|pane| pane.read(cx).is_zoomed());
 
         let active = self.panes.panes().into_iter().next();
-        let x = if let Some(ref zoomed_pane) = zoomed_pane {
+        let pane = if let Some(ref zoomed_pane) = zoomed_pane {
             zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
         } else if let Some(active) = active {
             self.panes
@@ -122,7 +122,7 @@ impl Render for RunningState {
             .size_full()
             .key_context("DebugSessionItem")
             .track_focus(&self.focus_handle(cx))
-            .child(h_flex().flex_1().child(x))
+            .child(h_flex().flex_1().child(pane))
     }
 }
 

crates/ui/src/components/context_menu.rs 🔗

@@ -154,7 +154,6 @@ pub struct ContextMenu {
     key_context: SharedString,
     _on_blur_subscription: Subscription,
     keep_open_on_confirm: bool,
-    eager: bool,
     documentation_aside: Option<(usize, DocumentationAside)>,
     fixed_width: Option<DefiniteLength>,
 }
@@ -207,7 +206,6 @@ impl ContextMenu {
                     key_context: "menu".into(),
                     _on_blur_subscription,
                     keep_open_on_confirm: false,
-                    eager: false,
                     documentation_aside: None,
                     fixed_width: None,
                     end_slot_action: None,
@@ -250,43 +248,6 @@ impl ContextMenu {
                     key_context: "menu".into(),
                     _on_blur_subscription,
                     keep_open_on_confirm: true,
-                    eager: false,
-                    documentation_aside: None,
-                    fixed_width: None,
-                    end_slot_action: None,
-                },
-                window,
-                cx,
-            )
-        })
-    }
-
-    pub fn build_eager(
-        window: &mut Window,
-        cx: &mut App,
-        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
-    ) -> Entity<Self> {
-        cx.new(|cx| {
-            let focus_handle = cx.focus_handle();
-            let _on_blur_subscription = cx.on_blur(
-                &focus_handle,
-                window,
-                |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
-            );
-            window.refresh();
-            f(
-                Self {
-                    builder: None,
-                    items: Default::default(),
-                    focus_handle,
-                    action_context: None,
-                    selected_index: None,
-                    delayed: false,
-                    clicked: false,
-                    key_context: "menu".into(),
-                    _on_blur_subscription,
-                    keep_open_on_confirm: false,
-                    eager: true,
                     documentation_aside: None,
                     fixed_width: None,
                     end_slot_action: None,
@@ -327,7 +288,6 @@ impl ContextMenu {
                     |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
                 ),
                 keep_open_on_confirm: false,
-                eager: false,
                 documentation_aside: None,
                 fixed_width: None,
                 end_slot_action: None,
@@ -634,10 +594,7 @@ impl ContextMenu {
                 ..
             })
             | ContextMenuItem::CustomEntry { handler, .. },
-        ) = self
-            .selected_index
-            .and_then(|ix| self.items.get(ix))
-            .filter(|_| !self.eager)
+        ) = self.selected_index.and_then(|ix| self.items.get(ix))
         {
             (handler)(context, window, cx)
         }
@@ -740,10 +697,9 @@ impl ContextMenu {
     fn select_index(
         &mut self,
         ix: usize,
-        window: &mut Window,
-        cx: &mut Context<Self>,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
     ) -> Option<usize> {
-        let context = self.action_context.as_ref();
         self.documentation_aside = None;
         let item = self.items.get(ix)?;
         if item.is_selectable() {
@@ -752,9 +708,6 @@ impl ContextMenu {
                 if let Some(callback) = &entry.documentation_aside {
                     self.documentation_aside = Some((ix, callback.clone()));
                 }
-                if self.eager && !entry.disabled {
-                    (entry.handler)(context, window, cx)
-                }
             }
         }
         Some(ix)

crates/ui/src/components/dropdown_menu.rs 🔗

@@ -2,6 +2,8 @@ use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
 
 use crate::{ContextMenu, PopoverMenu, prelude::*};
 
+use super::PopoverMenuHandle;
+
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum DropdownStyle {
     #[default]
@@ -22,6 +24,7 @@ pub struct DropdownMenu {
     menu: Entity<ContextMenu>,
     full_width: bool,
     disabled: bool,
+    handle: Option<PopoverMenuHandle<ContextMenu>>,
 }
 
 impl DropdownMenu {
@@ -37,6 +40,7 @@ impl DropdownMenu {
             menu,
             full_width: false,
             disabled: false,
+            handle: None,
         }
     }
 
@@ -52,6 +56,7 @@ impl DropdownMenu {
             menu,
             full_width: false,
             disabled: false,
+            handle: None,
         }
     }
 
@@ -64,6 +69,11 @@ impl DropdownMenu {
         self.full_width = full_width;
         self
     }
+
+    pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
+        self.handle = Some(handle);
+        self
+    }
 }
 
 impl Disableable for DropdownMenu {
@@ -85,6 +95,7 @@ impl RenderOnce for DropdownMenu {
                     .style(self.style),
             )
             .attach(Corner::BottomLeft)
+            .when_some(self.handle.clone(), |el, handle| el.with_handle(handle))
     }
 }
 
@@ -159,17 +170,11 @@ pub struct DropdownTriggerStyle {
 impl DropdownTriggerStyle {
     pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
         let colors = cx.theme().colors();
-
-        if style == DropdownStyle::Solid {
-            Self {
-                // why is this editor_background?
-                bg: colors.editor_background,
-            }
-        } else {
-            Self {
-                bg: colors.ghost_element_background,
-            }
-        }
+        let bg = match style {
+            DropdownStyle::Solid => colors.editor_background,
+            DropdownStyle::Ghost => colors.ghost_element_background,
+        };
+        Self { bg }
     }
 }