debugger: Add horizontal scroll bars to variable list, memory view, and breakpoint list (#41594)

Closes #40360

This PR added heuristics to determine what variable/breakpoint list
entry has the longest width when rendered. I added this in so the
uniform list would correctly determine which item has the longest width
and use that to calculate the scrollbar size.

The heuristic can be off if a non-mono space font is used in the UI; in
most cases, it's more than accurate enough though.

Release Notes:

- debugger: Add horizontal scroll bars to variable list, memory view,
and breakpoint list

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
This commit is contained in:
Anthony Eid
2025-10-30 19:43:32 +00:00
committed by GitHub
co-authored by MrSubidubi
parent 3944234bab
commit 44e5a962e6
6 changed files with 105 additions and 11 deletions
@@ -12,6 +12,7 @@ use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use itertools::Itertools;
use language::Point;
use project::{
Project,
@@ -24,7 +25,7 @@ use project::{
};
use ui::{
Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
ScrollAxes, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
};
use util::rel_path::RelPath;
use workspace::Workspace;
@@ -55,6 +56,7 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
max_width_index: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
@@ -95,6 +97,7 @@ impl BreakpointList {
dap_store,
worktree_store,
breakpoints: Default::default(),
max_width_index: None,
workspace,
session,
focus_handle,
@@ -570,6 +573,8 @@ impl BreakpointList {
.collect()
}),
)
.with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
.with_width_from_item(self.max_width_index)
.track_scroll(self.scroll_handle.clone())
.flex_1()
}
@@ -732,6 +737,26 @@ impl Render for BreakpointList {
.chain(exception_breakpoints),
);
let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
self.max_width_index = self
.breakpoints
.iter()
.map(|entry| match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_bp) => {
let name_and_line = format!("{}:{}", line_bp.name, line_bp.line);
let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0);
(name_and_line.len() + dir_len) as f32 * text_pixels
}
BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => {
exc_bp.data.label.len() as f32 * text_pixels
}
BreakpointEntryKind::DataBreakpoint(data_bp) => {
data_bp.0.context.human_readable_label().len() as f32 * text_pixels
}
})
.position_max_by(|left, right| left.total_cmp(right));
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
@@ -749,7 +774,14 @@ impl Render for BreakpointList {
.size_full()
.pt_1()
.child(self.render_list(cx))
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.custom_scrollbars(
ui::Scrollbars::new(ScrollAxes::Both)
.tracked_scroll_handle(self.scroll_handle.clone())
.with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
.tracked_entity(cx.entity_id()),
window,
cx,
)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal().color(DividerColor::Border))
.child(
@@ -10,8 +10,9 @@ use std::{
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
Focusable, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle,
UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
Focusable, ListHorizontalSizingBehavior, MouseButton, Point, ScrollStrategy, ScrollWheelEvent,
Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
anchored, deferred, uniform_list,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
@@ -229,6 +230,7 @@ impl MemoryView {
},
)
.track_scroll(view_state.scroll_handle)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
let mut view_state = this.view_state();
let delta = evt.delta.pixel_delta(window.line_height());
@@ -917,7 +919,17 @@ impl Render for MemoryView {
)
.with_priority(1)
}))
.vertical_scrollbar_for(self.view_state_handle.clone(), window, cx),
.custom_scrollbars(
ui::Scrollbars::new(ui::ScrollAxes::Both)
.tracked_scroll_handle(self.view_state_handle.clone())
.with_track_along(
ui::ScrollAxes::Both,
cx.theme().colors().panel_background,
)
.tracked_entity(cx.entity_id()),
window,
cx,
),
)
}
}
@@ -11,15 +11,18 @@ use gpui::{
FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement,
UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use itertools::Itertools;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::{
dap_command::DataBreakpointContext,
session::{Session, SessionEvent, Watcher},
};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use ui::{ContextMenu, ListItem, ScrollAxes, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use util::{debug_panic, maybe};
static INDENT_STEP_SIZE: Pixels = px(10.0);
actions!(
variable_list,
[
@@ -185,6 +188,7 @@ struct VariableColor {
pub struct VariableList {
entries: Vec<ListEntry>,
max_width_index: Option<usize>,
entry_states: HashMap<EntryPath, EntryState>,
selected_stack_frame_id: Option<StackFrameId>,
list_handle: UniformListScrollHandle,
@@ -243,6 +247,7 @@ impl VariableList {
disabled: false,
edited_path: None,
entries: Default::default(),
max_width_index: None,
entry_states: Default::default(),
weak_running,
memory_view,
@@ -368,6 +373,26 @@ impl VariableList {
}
self.entries = entries;
let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
let indent_size = INDENT_STEP_SIZE.to_f64() as f32;
self.max_width_index = self
.entries
.iter()
.map(|entry| match &entry.entry {
DapEntry::Scope(scope) => scope.name.len() as f32 * text_pixels,
DapEntry::Variable(variable) => {
(variable.value.len() + variable.name.len()) as f32 * text_pixels
+ (entry.path.indices.len() as f32 * indent_size)
}
DapEntry::Watcher(watcher) => {
(watcher.value.len() + watcher.expression.len()) as f32 * text_pixels
+ (entry.path.indices.len() as f32 * indent_size)
}
})
.position_max_by(|left, right| left.total_cmp(right));
cx.notify();
}
@@ -1244,7 +1269,7 @@ impl VariableList {
.disabled(self.disabled)
.selectable(false)
.indent_level(state.depth)
.indent_step_size(px(10.))
.indent_step_size(INDENT_STEP_SIZE)
.always_show_disclosure_icon(true)
.when(var_ref > 0, |list_item| {
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1445,7 +1470,7 @@ impl VariableList {
.disabled(self.disabled)
.selectable(false)
.indent_level(state.depth)
.indent_step_size(px(10.))
.indent_step_size(INDENT_STEP_SIZE)
.always_show_disclosure_icon(true)
.when(var_ref > 0, |list_item| {
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1507,7 +1532,6 @@ impl Render for VariableList {
.key_context("VariableList")
.id("variable-list")
.group("variable-list")
.overflow_y_scroll()
.size_full()
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
@@ -1533,6 +1557,9 @@ impl Render for VariableList {
}),
)
.track_scroll(self.list_handle.clone())
.with_width_from_item(self.max_width_index)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
.gap_1_5()
.size_full()
.flex_grow(),
@@ -1546,7 +1573,15 @@ impl Render for VariableList {
)
.with_priority(1)
}))
.vertical_scrollbar_for(self.list_handle.clone(), window, cx)
// .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
.custom_scrollbars(
ui::Scrollbars::new(ScrollAxes::Both)
.tracked_scroll_handle(self.list_handle.clone())
.with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
.tracked_entity(cx.entity_id()),
window,
cx,
)
}
}
+2
View File
@@ -251,6 +251,8 @@ impl Element for UniformList {
None
}
// self.max_found_width = 0.0
//
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
+1 -1
View File
@@ -392,7 +392,7 @@ pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
impl Scrollbars {
pub fn new(show_along: ScrollAxes) -> Self {
Self::new_with_setting(show_along, |_| ShowScrollbar::default())
Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
}
pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars {
+13
View File
@@ -144,6 +144,19 @@ impl TextSize {
Self::Editor => rems_from_px(theme_settings.buffer_font_size(cx)),
}
}
pub fn pixels(self, cx: &App) -> Pixels {
let theme_settings = ThemeSettings::get_global(cx);
match self {
Self::Large => px(16.),
Self::Default => px(14.),
Self::Small => px(12.),
Self::XSmall => px(10.),
Self::Ui => theme_settings.ui_font_size(cx),
Self::Editor => theme_settings.buffer_font_size(cx),
}
}
}
/// The size of a [`Headline`] element