Start improving support for keyboard-driven debugging (#29380)

Cole Miller , Piotr Osiewicz , and Anthony Eid created

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

assets/keymaps/vim.json                                    |   2 
crates/debugger_ui/src/debugger_panel.rs                   | 264 +++++++
crates/debugger_ui/src/debugger_ui.rs                      |   7 
crates/debugger_ui/src/session/running.rs                  |  50 +
crates/debugger_ui/src/session/running/breakpoint_list.rs  |   2 
crates/debugger_ui/src/session/running/console.rs          |  14 
crates/debugger_ui/src/session/running/stack_frame_list.rs |   8 
crates/debugger_ui/src/session/running/variable_list.rs    |   2 
crates/debugger_ui/src/tests/module_list.rs                |   2 
crates/project/src/debugger/dap_store.rs                   |   1 
10 files changed, 307 insertions(+), 45 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -693,7 +693,7 @@
     }
   },
   {
-    "context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
+    "context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
     "bindings": {
       // window related commands (ctrl-w X)
       "ctrl-w": null,

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1,7 +1,9 @@
 use crate::persistence::DebuggerPaneItem;
 use crate::{
-    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
-    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
+    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, FocusBreakpointList,
+    FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
+    Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
+    persistence,
 };
 use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
@@ -38,6 +40,7 @@ use task::{
 };
 use terminal_view::TerminalView;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
+use workspace::SplitDirection;
 use workspace::{
     Pane, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
@@ -790,6 +793,7 @@ impl DebugPanel {
 
     fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
         let active_session = self.active_session.clone();
+        let focus_handle = self.focus_handle.clone();
 
         Some(
             h_flex()
@@ -821,8 +825,17 @@ impl DebugPanel {
                                                     this.pause_thread(cx);
                                                 },
                                             ))
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Pause program")(window, cx)
+                                            .tooltip({
+                                                let focus_handle = focus_handle.clone();
+                                                move |window, cx| {
+                                                    Tooltip::for_action_in(
+                                                        "Pause program",
+                                                        &Pause,
+                                                        &focus_handle,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                }
                                             }),
                                     )
                                 } else {
@@ -835,8 +848,17 @@ impl DebugPanel {
                                                 |this, _, _window, cx| this.continue_thread(cx),
                                             ))
                                             .disabled(thread_status != ThreadStatus::Stopped)
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Continue program")(window, cx)
+                                            .tooltip({
+                                                let focus_handle = focus_handle.clone();
+                                                move |window, cx| {
+                                                    Tooltip::for_action_in(
+                                                        "Continue program",
+                                                        &Continue,
+                                                        &focus_handle,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                }
                                             }),
                                     )
                                 }
@@ -852,8 +874,17 @@ impl DebugPanel {
                                         },
                                     ))
                                     .disabled(thread_status != ThreadStatus::Stopped)
-                                    .tooltip(move |window, cx| {
-                                        Tooltip::text("Step over")(window, cx)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                "Step over",
+                                                &StepOver,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                             .child(
@@ -867,8 +898,17 @@ impl DebugPanel {
                                         },
                                     ))
                                     .disabled(thread_status != ThreadStatus::Stopped)
-                                    .tooltip(move |window, cx| {
-                                        Tooltip::text("Step out")(window, cx)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                "Step out",
+                                                &StepOut,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                             .child(
@@ -882,8 +922,17 @@ impl DebugPanel {
                                         },
                                     ))
                                     .disabled(thread_status != ThreadStatus::Stopped)
-                                    .tooltip(move |window, cx| {
-                                        Tooltip::text("Step in")(window, cx)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                "Step in",
+                                                &StepInto,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                             .child(Divider::vertical())
@@ -916,8 +965,17 @@ impl DebugPanel {
                                             this.toggle_ignore_breakpoints(cx);
                                         },
                                     ))
-                                    .tooltip(move |window, cx| {
-                                        Tooltip::text("Disable all breakpoints")(window, cx)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                "Disable all breakpoints",
+                                                &ToggleIgnoreBreakpoints,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                             .child(Divider::vertical())
@@ -930,8 +988,17 @@ impl DebugPanel {
                                             this.restart_session(cx);
                                         },
                                     ))
-                                    .tooltip(move |window, cx| {
-                                        Tooltip::text("Restart")(window, cx)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                "Restart",
+                                                &Restart,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                             .child(
@@ -948,15 +1015,24 @@ impl DebugPanel {
                                             && thread_status != ThreadStatus::Running,
                                     )
                                     .tooltip({
+                                        let focus_handle = focus_handle.clone();
                                         let label = if capabilities
                                             .supports_terminate_threads_request
                                             .unwrap_or_default()
                                         {
                                             "Terminate Thread"
                                         } else {
-                                            "Terminate all Threads"
+                                            "Terminate All Threads"
                                         };
-                                        move |window, cx| Tooltip::text(label)(window, cx)
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                label,
+                                                &Stop,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
                                     }),
                             )
                         },
@@ -1006,19 +1082,57 @@ impl DebugPanel {
                                         });
                                     }
                                 })
-                                .tooltip(|window, cx| {
-                                    Tooltip::for_action(
-                                        "New Debug Session",
-                                        &CreateDebuggingSession,
-                                        window,
-                                        cx,
-                                    )
+                                .tooltip({
+                                    let focus_handle = focus_handle.clone();
+                                    move |window, cx| {
+                                        Tooltip::for_action_in(
+                                            "New Debug Session",
+                                            &CreateDebuggingSession,
+                                            &focus_handle,
+                                            window,
+                                            cx,
+                                        )
+                                    }
                                 }),
                         ),
                 ),
         )
     }
 
+    fn activate_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(session) = self.active_session() {
+            session.update(cx, |session, cx| {
+                if let Some(running) = session.mode().as_running() {
+                    running.update(cx, |running, cx| {
+                        running.activate_pane_in_direction(direction, window, cx);
+                    })
+                }
+            })
+        }
+    }
+
+    fn activate_item(
+        &mut self,
+        item: DebuggerPaneItem,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(session) = self.active_session() {
+            session.update(cx, |session, cx| {
+                if let Some(running) = session.mode().as_running() {
+                    running.update(cx, |running, cx| {
+                        running.activate_item(item, window, cx);
+                    })
+                }
+            })
+        }
+    }
+
     fn activate_session(
         &mut self,
         session_item: Entity<DebugSession>,
@@ -1111,6 +1225,7 @@ impl Panel for DebugPanel {
 impl Render for DebugPanel {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let has_sessions = self.sessions.len() > 0;
+        let this = cx.weak_entity();
         debug_assert_eq!(has_sessions, self.active_session.is_some());
 
         if self
@@ -1128,6 +1243,105 @@ impl Render for DebugPanel {
             .key_context("DebugPanel")
             .child(h_flex().children(self.top_controls_strip(window, cx)))
             .track_focus(&self.focus_handle(cx))
+            .on_action({
+                let this = this.clone();
+                move |_: &workspace::ActivatePaneLeft, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &workspace::ActivatePaneRight, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &workspace::ActivatePaneUp, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &workspace::ActivatePaneDown, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusConsole, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::Console, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusVariables, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusBreakpointList, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusFrames, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusModules, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusLoadedSources, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
+                    })
+                    .ok();
+                }
+            })
+            .on_action({
+                let this = this.clone();
+                move |_: &FocusTerminal, window, cx| {
+                    this.update(cx, |this, cx| {
+                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
+                    })
+                    .ok();
+                }
+            })
             .when(self.active_session.is_some(), |this| {
                 this.on_mouse_down(
                     MouseButton::Right,

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -35,6 +35,13 @@ actions!(
         ToggleIgnoreBreakpoints,
         ClearAllBreakpoints,
         CreateDebuggingSession,
+        FocusConsole,
+        FocusVariables,
+        FocusBreakpointList,
+        FocusFrames,
+        FocusModules,
+        FocusLoadedSources,
+        FocusTerminal,
     ]
 );
 

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

@@ -37,8 +37,8 @@ use ui::{
 use util::ResultExt;
 use variable_list::VariableList;
 use workspace::{
-    ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, Workspace,
-    item::TabContentParams, move_item, pane::Event,
+    ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, SplitDirection,
+    Workspace, item::TabContentParams, move_item, pane::Event,
 };
 
 pub struct RunningState {
@@ -57,6 +57,7 @@ pub struct RunningState {
     _console: Entity<Console>,
     breakpoint_list: Entity<BreakpointList>,
     panes: PaneGroup,
+    active_pane: Option<Entity<Pane>>,
     pane_close_subscriptions: HashMap<EntityId, Subscription>,
     _schedule_serialize: Option<Task<()>>,
 }
@@ -167,8 +168,14 @@ impl Item for SubView {
 }
 
 impl Render for SubView {
-    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
-        v_flex().size_full().child(self.inner.clone())
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex()
+            .size_full()
+            .when(self.pane_focus_handle.contains_focused(window, cx), |el| {
+                // TODO better way of showing focus?
+                el.border_1().border_color(gpui::red())
+            })
+            .child(self.inner.clone())
     }
 }
 
@@ -576,6 +583,7 @@ impl RunningState {
             stack_frame_list,
             session_id,
             panes,
+            active_pane: None,
             module_list,
             _console: console,
             breakpoint_list,
@@ -616,7 +624,7 @@ impl RunningState {
     fn create_sub_view(
         &self,
         item_kind: DebuggerPaneItem,
-        pane: &Entity<Pane>,
+        _pane: &Entity<Pane>,
         cx: &mut Context<Self>,
     ) -> Box<dyn ItemHandle> {
         match item_kind {
@@ -624,7 +632,7 @@ impl RunningState {
                 let weak_console = self._console.clone().downgrade();
 
                 Box::new(SubView::new(
-                    pane.focus_handle(cx),
+                    self._console.focus_handle(cx),
                     self._console.clone().into(),
                     item_kind,
                     Some(Box::new(move |cx| {
@@ -784,6 +792,9 @@ impl RunningState {
                 debug_assert!(_did_find_pane);
                 cx.notify();
             }
+            Event::Focus => {
+                this.active_pane = Some(source_pane.clone());
+            }
             Event::ZoomIn => {
                 source_pane.update(cx, |pane, cx| {
                     pane.set_zoomed(true, cx);
@@ -800,6 +811,27 @@ impl RunningState {
         }
     }
 
+    pub(crate) fn activate_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(pane) = self
+            .active_pane
+            .as_ref()
+            .and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
+        {
+            window.focus(&pane.focus_handle(cx));
+        } else {
+            self.workspace
+                .update(cx, |workspace, cx| {
+                    workspace.activate_pane_in_direction(direction, window, cx)
+                })
+                .ok();
+        }
+    }
+
     pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
         if self.thread_id.is_some() {
             self.stack_frame_list
@@ -838,8 +870,7 @@ impl RunningState {
         &self.module_list
     }
 
-    #[cfg(test)]
-    pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
+    pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
         let (variable_list_position, pane) = self
             .panes
             .panes()
@@ -847,7 +878,7 @@ impl RunningState {
             .find_map(|pane| {
                 pane.read(cx)
                     .items_of_type::<SubView>()
-                    .position(|view| view.read(cx).view_kind().to_shared_string() == *"Modules")
+                    .position(|view| view.read(cx).view_kind() == item)
                     .map(|view| (view, pane))
             })
             .unwrap();
@@ -855,6 +886,7 @@ impl RunningState {
             this.activate_item(variable_list_position, true, true, window, cx);
         })
     }
+
     #[cfg(test)]
     pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
         &self.variable_list

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

@@ -45,6 +45,7 @@ impl Focusable for BreakpointList {
         self.focus_handle.clone()
     }
 }
+
 impl BreakpointList {
     pub(super) fn new(
         session: Entity<Session>,
@@ -213,6 +214,7 @@ impl Render for BreakpointList {
         }
         v_flex()
             .id("breakpoint-list")
+            .track_focus(&self.focus_handle)
             .on_hover(cx.listener(|this, hovered, window, cx| {
                 if *hovered {
                     this.show_scrollbar = true;

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

@@ -7,7 +7,9 @@ use collections::HashMap;
 use dap::OutputEvent;
 use editor::{CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
 use fuzzy::StringMatchCandidate;
-use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
+use gpui::{
+    Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
+};
 use language::{Buffer, CodeLabel, ToOffset};
 use menu::Confirm;
 use project::{
@@ -28,6 +30,7 @@ pub struct Console {
     stack_frame_list: Entity<StackFrameList>,
     last_token: OutputToken,
     update_output_task: Task<()>,
+    focus_handle: FocusHandle,
 }
 
 impl Console {
@@ -56,6 +59,7 @@ impl Console {
             editor.set_show_edit_predictions(Some(false), window, cx);
             editor
         });
+        let focus_handle = cx.focus_handle();
 
         let this = cx.weak_entity();
         let query_bar = cx.new(|cx| {
@@ -82,6 +86,7 @@ impl Console {
             stack_frame_list,
             update_output_task: Task::ready(()),
             last_token: OutputToken(0),
+            focus_handle,
         }
     }
 
@@ -230,6 +235,7 @@ impl Render for Console {
         });
 
         v_flex()
+            .track_focus(&self.focus_handle)
             .key_context("DebugConsole")
             .on_action(cx.listener(Self::evaluate))
             .size_full()
@@ -242,6 +248,12 @@ impl Render for Console {
     }
 }
 
+impl Focusable for Console {
+    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
 struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
 
 impl CompletionProvider for ConsoleQueryBarCompletionProvider {

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

@@ -132,13 +132,6 @@ impl StackFrameList {
             .collect()
     }
 
-    pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
-        self.stack_frames(cx)
-            .first()
-            .map(|stack_frame| stack_frame.dap.id)
-            .unwrap_or(0)
-    }
-
     pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
         self.selected_stack_frame_id
     }
@@ -557,6 +550,7 @@ impl StackFrameList {
 impl Render for StackFrameList {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         div()
+            .track_focus(&self.focus_handle)
             .size_full()
             .p_1()
             .child(list(self.list.clone()).size_full())

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

@@ -929,12 +929,12 @@ impl Render for VariableList {
         self.build_entries(cx);
 
         v_flex()
+            .track_focus(&self.focus_handle)
             .key_context("VariableList")
             .id("variable-list")
             .group("variable-list")
             .overflow_y_scroll()
             .size_full()
-            .track_focus(&self.focus_handle(cx))
             .on_action(cx.listener(Self::select_first))
             .on_action(cx.listener(Self::select_last))
             .on_action(cx.listener(Self::select_prev))

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

@@ -112,7 +112,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
         });
 
     running_state.update_in(cx, |this, window, cx| {
-        this.activate_modules_list(window, cx);
+        this.activate_item(crate::persistence::DebuggerPaneItem::Modules, window, cx);
         cx.refresh_windows();
     });
 

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

@@ -55,6 +55,7 @@ use task::{DebugTaskDefinition, DebugTaskTemplate};
 use util::ResultExt as _;
 use worktree::Worktree;
 
+#[derive(Debug)]
 pub enum DapStoreEvent {
     DebugClientStarted(SessionId),
     DebugSessionInitialized(SessionId),