debugger_ui: Improve some elements of the UI (#43344)

Danilo Leal created

- In the launch tab of the new session mode, I've switched it to use the
`InputField` component instead given that had all that we needed
already. Allows for removing a good chunk of editor-related code
- Also in the launch tab, added support for keyboard navigation between
all of the elements there (dropdown, inputs, and switch component)
- Added some simple an empty state treatment for the breakpoint column
when there are none set


https://github.com/user-attachments/assets/a441aa8a-360b-4e38-839f-786315a8a235

Release Notes:

- debugger: Made the input elements within the launch tab in the new
session modal keyboard navigableΛ™.

Change summary

Cargo.lock                                                |   1 
crates/debugger_ui/Cargo.toml                             |   1 
crates/debugger_ui/src/debugger_panel.rs                  |  33 +
crates/debugger_ui/src/new_process_modal.rs               | 133 +++-----
crates/debugger_ui/src/session/running/breakpoint_list.rs |   1 
crates/ui_input/src/input_field.rs                        |  12 
6 files changed, 88 insertions(+), 93 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -4770,6 +4770,7 @@ dependencies = [
  "tree-sitter-go",
  "tree-sitter-json",
  "ui",
+ "ui_input",
  "unindent",
  "util",
  "workspace",

crates/debugger_ui/Cargo.toml πŸ”—

@@ -70,6 +70,7 @@ theme.workspace = true
 tree-sitter-json.workspace = true
 tree-sitter.workspace = true
 ui.workspace = true
+ui_input.workspace = true
 unindent = { workspace = true, optional = true }
 util.workspace = true
 workspace.workspace = true

crates/debugger_ui/src/debugger_panel.rs πŸ”—

@@ -1692,7 +1692,7 @@ impl Render for DebugPanel {
                         .child(
                             Button::new("spawn-new-session-empty-state", "New Session")
                                 .icon(IconName::Plus)
-                                .icon_size(IconSize::XSmall)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Muted)
                                 .icon_position(IconPosition::Start)
                                 .on_click(|_, window, cx| {
@@ -1702,8 +1702,7 @@ impl Render for DebugPanel {
                         .child(
                             Button::new("edit-debug-settings", "Edit debug.json")
                                 .icon(IconName::Code)
-                                .icon_size(IconSize::XSmall)
-                                .color(Color::Muted)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Muted)
                                 .icon_position(IconPosition::Start)
                                 .on_click(|_, window, cx| {
@@ -1716,8 +1715,7 @@ impl Render for DebugPanel {
                         .child(
                             Button::new("open-debugger-docs", "Debugger Docs")
                                 .icon(IconName::Book)
-                                .color(Color::Muted)
-                                .icon_size(IconSize::XSmall)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Muted)
                                 .icon_position(IconPosition::Start)
                                 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
@@ -1728,8 +1726,7 @@ impl Render for DebugPanel {
                                 "Debugger Extensions",
                             )
                             .icon(IconName::Blocks)
-                            .color(Color::Muted)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
                             .icon_position(IconPosition::Start)
                             .on_click(|_, window, cx| {
@@ -1746,6 +1743,15 @@ impl Render for DebugPanel {
                             }),
                         );
 
+                    let has_breakpoints = self
+                        .project
+                        .read(cx)
+                        .breakpoint_store()
+                        .read(cx)
+                        .all_source_breakpoints(cx)
+                        .values()
+                        .any(|breakpoints| !breakpoints.is_empty());
+
                     let breakpoint_list = v_flex()
                         .group("base-breakpoint-list")
                         .when_else(
@@ -1769,7 +1775,18 @@ impl Render for DebugPanel {
                                     ),
                                 ),
                         )
-                        .child(self.breakpoint_list.clone());
+                        .when(has_breakpoints, |this| {
+                            this.child(self.breakpoint_list.clone())
+                        })
+                        .when(!has_breakpoints, |this| {
+                            this.child(
+                                v_flex().size_full().items_center().justify_center().child(
+                                    Label::new("No Breakpoints Set")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted),
+                                ),
+                            )
+                        });
 
                     this.child(
                         v_flex()

crates/debugger_ui/src/new_process_modal.rs πŸ”—

@@ -12,23 +12,22 @@ use tasks_ui::{TaskOverrides, TasksModal};
 use dap::{
     DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
 };
-use editor::{Editor, EditorElement, EditorStyle};
+use editor::Editor;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
+    KeyContext, Render, Subscription, Task, WeakEntity,
 };
 use itertools::Itertools as _;
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
-use settings::Settings;
 use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig};
-use theme::ThemeSettings;
 use ui::{
     ContextMenu, DropdownMenu, FluentBuilder, IconWithIndicator, Indicator, KeyBinding, ListItem,
     ListItemSpacing, Switch, SwitchLabelPosition, ToggleButtonGroup, ToggleButtonSimple,
     ToggleState, Tooltip, prelude::*,
 };
+use ui_input::InputField;
 use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
 use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
 
@@ -448,7 +447,7 @@ impl NewProcessModal {
         &mut self,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> ui::DropdownMenu {
+    ) -> DropdownMenu {
         let workspace = self.workspace.clone();
         let weak = cx.weak_entity();
         let active_buffer = self.task_contexts(cx).and_then(|tc| {
@@ -508,6 +507,13 @@ impl NewProcessModal {
                 menu
             }),
         )
+        .style(ui::DropdownStyle::Outlined)
+        .tab_index(0)
+        .attach(gpui::Corner::BottomLeft)
+        .offset(gpui::Point {
+            x: px(0.0),
+            y: px(2.0),
+        })
     }
 }
 
@@ -540,44 +546,6 @@ impl Focusable for NewProcessMode {
     }
 }
 
-fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
-    let settings = ThemeSettings::get_global(cx);
-    let theme = cx.theme();
-
-    let text_style = TextStyle {
-        color: cx.theme().colors().text,
-        font_family: settings.buffer_font.family.clone(),
-        font_features: settings.buffer_font.features.clone(),
-        font_size: settings.buffer_font_size(cx).into(),
-        font_weight: settings.buffer_font.weight,
-        line_height: relative(settings.buffer_line_height.value()),
-        background_color: Some(theme.colors().editor_background),
-        ..Default::default()
-    };
-
-    let element = EditorElement::new(
-        editor,
-        EditorStyle {
-            background: theme.colors().editor_background,
-            local_player: theme.players().local(),
-            text: text_style,
-            ..Default::default()
-        },
-    );
-
-    div()
-        .rounded_md()
-        .p_1()
-        .border_1()
-        .border_color(theme.colors().border_variant)
-        .when(
-            editor.focus_handle(cx).contains_focused(window, cx),
-            |this| this.border_color(theme.colors().border_focused),
-        )
-        .child(element)
-        .bg(theme.colors().editor_background)
-}
-
 impl Render for NewProcessModal {
     fn render(
         &mut self,
@@ -788,22 +756,26 @@ impl RenderOnce for AttachMode {
 
 #[derive(Clone)]
 pub(super) struct ConfigureMode {
-    program: Entity<Editor>,
-    cwd: Entity<Editor>,
+    program: Entity<InputField>,
+    cwd: Entity<InputField>,
     stop_on_entry: ToggleState,
     save_to_debug_json: ToggleState,
 }
 
 impl ConfigureMode {
     pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
-        let program = cx.new(|cx| Editor::single_line(window, cx));
-        program.update(cx, |this, cx| {
-            this.set_placeholder_text("ENV=Zed ~/bin/program --option", window, cx);
+        let program = cx.new(|cx| {
+            InputField::new(window, cx, "ENV=Zed ~/bin/program --option")
+                .label("Program")
+                .tab_stop(true)
+                .tab_index(1)
         });
 
-        let cwd = cx.new(|cx| Editor::single_line(window, cx));
-        cwd.update(cx, |this, cx| {
-            this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", window, cx);
+        let cwd = cx.new(|cx| {
+            InputField::new(window, cx, "Ex: $ZED_WORKTREE_ROOT")
+                .label("Working Directory")
+                .tab_stop(true)
+                .tab_index(2)
         });
 
         cx.new(|_| Self {
@@ -815,9 +787,9 @@ impl ConfigureMode {
     }
 
     fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
-        self.cwd.update(cx, |editor, cx| {
-            if editor.is_empty(cx) {
-                editor.set_text(cwd.to_string_lossy(), window, cx);
+        self.cwd.update(cx, |input_field, cx| {
+            if input_field.is_empty(cx) {
+                input_field.set_text(cwd.to_string_lossy(), window, cx);
             }
         });
     }
@@ -868,49 +840,44 @@ impl ConfigureMode {
         }
     }
 
+    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
+        window.focus_next();
+    }
+
+    fn on_tab_prev(
+        &mut self,
+        _: &menu::SelectPrevious,
+        window: &mut Window,
+        _: &mut Context<Self>,
+    ) {
+        window.focus_prev();
+    }
+
     fn render(
         &mut self,
         adapter_menu: DropdownMenu,
-        window: &mut Window,
+        _: &mut Window,
         cx: &mut ui::Context<Self>,
     ) -> impl IntoElement {
         v_flex()
+            .tab_group()
+            .track_focus(&self.program.focus_handle(cx))
+            .on_action(cx.listener(Self::on_tab))
+            .on_action(cx.listener(Self::on_tab_prev))
             .p_2()
             .w_full()
-            .gap_2()
-            .track_focus(&self.program.focus_handle(cx))
+            .gap_3()
             .child(
                 h_flex()
-                    .gap_2()
-                    .child(
-                        Label::new("Debugger")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
-                    )
+                    .gap_1()
+                    .child(Label::new("Debugger:").color(Color::Muted))
                     .child(adapter_menu),
             )
-            .child(
-                v_flex()
-                    .gap_0p5()
-                    .child(
-                        Label::new("Program")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(render_editor(&self.program, window, cx)),
-            )
-            .child(
-                v_flex()
-                    .gap_0p5()
-                    .child(
-                        Label::new("Working Directory")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(render_editor(&self.cwd, window, cx)),
-            )
+            .child(self.program.clone())
+            .child(self.cwd.clone())
             .child(
                 Switch::new("debugger-stop-on-entry", self.stop_on_entry)
+                    .tab_index(3_isize)
                     .label("Stop on Entry")
                     .label_position(SwitchLabelPosition::Start)
                     .label_size(LabelSize::Default)

crates/ui_input/src/input_field.rs πŸ”—

@@ -120,6 +120,11 @@ impl InputField {
         self.editor().read(cx).text(cx)
     }
 
+    pub fn clear(&self, window: &mut Window, cx: &mut App) {
+        self.editor()
+            .update(cx, |editor, cx| editor.clear(window, cx))
+    }
+
     pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
         self.editor()
             .update(cx, |editor, cx| editor.set_text(text, window, cx))
@@ -127,7 +132,8 @@ impl InputField {
 }
 
 impl Render for InputField {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let editor = self.editor.clone();
         let settings = ThemeSettings::get_global(cx);
         let theme_color = cx.theme().colors();
 
@@ -206,6 +212,10 @@ impl Render for InputField {
                     .bg(style.background_color)
                     .border_1()
                     .border_color(style.border_color)
+                    .when(
+                        editor.focus_handle(cx).contains_focused(window, cx),
+                        |this| this.border_color(theme_color.border_focused),
+                    )
                     .when_some(self.start_icon, |this, icon| {
                         this.gap_1()
                             .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))