T20 round 1 fixes

- the hex demo's selection stops at memory's end
- a disassembly gutter fills its row, so an empty one takes a press
- a breakpoint's box keeps its press from the row
- ruler ticks keep the places their step needs
- the toolbar names each usual key for the platform; the app binds them
- tests: gutter press, box press, tick places
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 02:54:23 -04:00
1 parent 74d6bd1771
commit 7a5d7e2ea6
9 files changed
+161 -34

No files matched your search

+2 -1
View File
@@ -245,4 +245,5 @@ Per-component lines live in `tasks/`. Tags there:
| T19 | 1 | FAIL | a merge rewrote line endings and added a last newline; the split view numbered its right side from the old file; an empty side started its hunk a line late; the subject counter covered long subjects; clipped code rows had no line height | | T19 | 1 | FAIL | a merge rewrote line endings and added a last newline; the split view numbered its right side from the old file; an empty side started its hunk a line late; the subject counter covered long subjects; clipped code rows had no line height |
| T19 | 2 | FAIL | a lone return split lines for similar but not for regions, so a change was lost; taking both sides added a newline to an empty side | | T19 | 2 | FAIL | a lone return split lines for similar but not for regions, so a change was lost; taking both sides added a newline to an empty side |
| T19 | 3 | PASS | | | T19 | 3 | PASS | |
| T20 | 1 | — | pending review | | T20 | 1 | FAIL | the demo picked past memory's end; an empty gutter had no height to press; a breakpoint's box also opened it; zoomed ticks lost their places; docs promised bound keys and batch actions |
| T20 | 2 | — | pending review |
+1 -1
View File
@@ -141,7 +141,7 @@ pub fn machine(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
frame().h(px(220.)).child( frame().h(px(220.)).child(
HexViewer::new("debug-hex", memory()) HexViewer::new("debug-hex", memory())
.base(0x7ffd_5a20_1000) .base(0x7ffd_5a20_1000)
.selected(now_picked..now_picked + 4) .selected(now_picked..(now_picked + 4).min(memory().len()))
.on_select(move |at, _, cx| set(&pick, at, cx)), .on_select(move |at, _, cx| set(&pick, at, cx)),
), ),
) )
+1 -1
View File
@@ -106,7 +106,7 @@ pub fn session(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (command, tell) = (state.clone(), said.clone()); let (command, tell) = (state.clone(), said.clone());
section( section(
"DebugToolbar", "DebugToolbar",
"The floating row that drives a session: continue or pause, the three steps while paused, restart and stop, each with its key.", "The floating row that drives a session: continue or pause, the three steps while paused, restart and stop. Each tooltip names the usual key; the app binds it.",
cx, cx,
) )
.child(row().child(DebugToolbar::new("debug-toolbar", now_state).on_command(move |which, _, cx| { .child(row().child(DebugToolbar::new("debug-toolbar", now_state).on_command(move |which, _, cx| {
+1
View File
@@ -15,6 +15,7 @@ mod sheet;
mod tests; mod tests;
pub use bars::{ActionBar, BulkActionBar}; pub use bars::{ActionBar, BulkActionBar};
pub(crate) use button::shortcut_text;
pub use button::{Button, ButtonVariant}; pub use button::{Button, ButtonVariant};
pub use confirm::{ConfirmButton, ConfirmMode}; pub use confirm::{ConfirmButton, ConfirmMode};
pub use copy::CopyButton; pub use copy::CopyButton;
+1 -1
View File
@@ -185,7 +185,6 @@ impl RenderOnce for Disassembly {
.id(row) .id(row)
.w_full() .w_full()
.flex() .flex()
.items_center()
.gap_3() .gap_3()
.whitespace_nowrap() .whitespace_nowrap()
.when(is_current, |line| line.bg(colors.warning.opacity(0.12))) .when(is_current, |line| line.bg(colors.warning.opacity(0.12)))
@@ -195,6 +194,7 @@ impl RenderOnce for Disassembly {
.flex_none() .flex_none()
.w_5() .w_5()
.flex() .flex()
.items_center()
.justify_center() .justify_center()
.cursor_pointer() .cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| { .on_mouse_down(MouseButton::Left, |_, window, _| {
+2
View File
@@ -8,6 +8,8 @@ mod hex;
mod network; mod network;
mod session; mod session;
mod stack; mod stack;
#[cfg(all(test, feature = "test-support"))]
mod tests;
mod timeline; mod timeline;
mod variables; mod variables;
+43 -28
View File
@@ -1,13 +1,14 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::{ use gpui::{
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, Keystroke,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
prelude::*,
}; };
use super::OnIndex; use super::OnIndex;
use crate::{ use crate::{
buttons::{ButtonVariant, IconButton}, buttons::{ButtonVariant, IconButton, shortcut_text},
forms::Checkbox, forms::Checkbox,
primitives::IconName, primitives::IconName,
theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize}, theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize},
@@ -36,7 +37,7 @@ pub enum DebugCommand {
type OnCommand = Rc<dyn Fn(DebugCommand, &mut Window, &mut App)>; type OnCommand = Rc<dyn Fn(DebugCommand, &mut Window, &mut App)>;
type OnSwitch = Rc<dyn Fn(usize, bool, &mut Window, &mut App)>; type OnSwitch = Rc<dyn Fn(usize, bool, &mut Window, &mut App)>;
/// The floating row that drives a debug session: continue or pause, the three steps while paused, restart and stop, each with its key. /// The floating row that drives a debug session: continue or pause, the three steps while paused, restart and stop. Each tooltip names the key debuggers use for it; the app binds the keys.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct DebugToolbar { pub struct DebugToolbar {
id: ElementId, id: ElementId,
@@ -67,20 +68,22 @@ impl RenderOnce for DebugToolbar {
let theme = cx.theme(); let theme = cx.theme();
let colors = theme.colors.clone(); let colors = theme.colors.clone();
let paused = self.state == DebugState::Paused; let paused = self.state == DebugState::Paused;
let button = |command: DebugCommand, icon: IconName, words: &'static str, enabled: bool| { let button =
let run = self.on_command.clone(); |command: DebugCommand, icon: IconName, words: &str, key: &str, enabled: bool| {
IconButton::new((self.id.clone(), format!("{command:?}")), icon) let run = self.on_command.clone();
.variant(ButtonVariant::Ghost) let key = Keystroke::parse(key).expect("a debug key parses");
.size(ControlSize::Sm) IconButton::new((self.id.clone(), format!("{command:?}")), icon)
.tooltip(words) .variant(ButtonVariant::Ghost)
.disabled(!enabled) .size(ControlSize::Sm)
.when_some(run, |button, run| { .tooltip(format!("{words} {}", shortcut_text(&key)))
button.on_click(move |_, window, cx| { .disabled(!enabled)
log::info!("debug toolbar: {command:?}"); .when_some(run, |button, run| {
run(command, window, cx) button.on_click(move |_, window, cx| {
log::info!("debug toolbar: {command:?}");
run(command, window, cx)
})
}) })
}) };
};
let rule = || div().w_0().h_4().border_l_1().border_color(colors.border); let rule = || div().w_0().h_4().border_l_1().border_color(colors.border);
div() div()
.flex() .flex()
@@ -93,39 +96,50 @@ impl RenderOnce for DebugToolbar {
.bg(colors.overlay) .bg(colors.overlay)
.shadow(theme.elevation(Elevation::Floating)) .shadow(theme.elevation(Elevation::Floating))
.child(if paused { .child(if paused {
button(DebugCommand::Continue, IconName::Play, "Continue F5", true) button(
DebugCommand::Continue,
IconName::Play,
"Continue",
"f5",
true,
)
} else { } else {
button(DebugCommand::Pause, IconName::Pause, "Pause F6", true) button(DebugCommand::Pause, IconName::Pause, "Pause", "f6", true)
}) })
.child(button( .child(button(
DebugCommand::StepOver, DebugCommand::StepOver,
IconName::RedoDot, IconName::RedoDot,
"Step over F10", "Step over",
"f10",
paused, paused,
)) ))
.child(button( .child(button(
DebugCommand::StepInto, DebugCommand::StepInto,
IconName::ArrowDownToDot, IconName::ArrowDownToDot,
"Step into F11", "Step into",
"f11",
paused, paused,
)) ))
.child(button( .child(button(
DebugCommand::StepOut, DebugCommand::StepOut,
IconName::ArrowUpFromDot, IconName::ArrowUpFromDot,
"Step out ⇧F11", "Step out",
"shift-f11",
paused, paused,
)) ))
.child(rule()) .child(rule())
.child(button( .child(button(
DebugCommand::Restart, DebugCommand::Restart,
IconName::RotateCw, IconName::RotateCw,
"Restart ⇧⌘F5", "Restart",
"secondary-shift-f5",
true, true,
)) ))
.child(button( .child(button(
DebugCommand::Stop, DebugCommand::Stop,
IconName::Square, IconName::Square,
"Stop ⇧F5", "Stop",
"shift-f5",
true, true,
)) ))
} }
@@ -142,7 +156,7 @@ pub struct Breakpoint {
pub hits: usize, pub hits: usize,
} }
/// Every breakpoint: turn each on or off, see its condition and hits, go to it, or remove it; or all at once. /// Every breakpoint: turn each on or off, see its condition and hits, go to it, or remove it.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct BreakpointList { pub struct BreakpointList {
id: ElementId, id: ElementId,
@@ -200,9 +214,10 @@ impl RenderOnce for BreakpointList {
let (open, remove) = (self.on_open.clone(), self.on_remove.clone()); let (open, remove) = (self.on_open.clone(), self.on_remove.clone());
let check = Checkbox::new((self.id.clone(), format!("on-{ix}")), point.enabled); let check = Checkbox::new((self.id.clone(), format!("on-{ix}")), point.enabled);
let check = match toggle { let check = match toggle {
Some(toggle) => { Some(toggle) => check.on_change(move |on, window, cx| {
check.on_change(move |on, window, cx| toggle(ix, on, window, cx)) cx.stop_propagation();
} toggle(ix, on, window, cx)
}),
None => check, None => check,
}; };
div() div()
+97
View File
@@ -0,0 +1,97 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext,
VisualTestContext, Window, div, point, px,
};
use super::{Breakpoint, BreakpointList, Disassembly, Instruction};
use crate::theme::Theme;
fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
/// Two plain instructions; it keeps the gutter presses.
struct Listing(Rc<RefCell<Vec<usize>>>);
impl Render for Listing {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let store = self.0.clone();
let plain = |address| Instruction {
address,
bytes: vec![0x90],
mnemonic: "nop".into(),
operands: "".into(),
comment: None,
source: None,
};
div().w(px(400.0)).h(px(200.0)).child(
Disassembly::new("listing", vec![plain(0x10), plain(0x11)])
.on_breakpoint(move |ix, _, _| store.borrow_mut().push(ix)),
)
}
}
#[gpui::test]
fn an_empty_gutter_takes_a_press_across_its_row(cx: &mut TestAppContext) {
cx.update(Theme::init);
let seen = Rc::new(RefCell::new(Vec::new()));
let store = seen.clone();
let (_, cx) = cx.add_window_view(|_, _| Listing(store));
settle(cx);
cx.simulate_click(point(px(10.0), px(2.0)), Modifiers::none());
settle(cx);
assert_eq!(*seen.borrow(), [0]);
}
/// One breakpoint; it keeps what its toggle and open report.
struct Points {
toggles: Rc<RefCell<Vec<(usize, bool)>>>,
opens: Rc<RefCell<Vec<usize>>>,
}
impl Render for Points {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (toggles, opens) = (self.toggles.clone(), self.opens.clone());
let point = Breakpoint {
path: "src/lift.rs".into(),
line: 3,
enabled: true,
condition: None,
hits: 0,
};
div().w(px(400.0)).child(
BreakpointList::new("points", [point])
.on_toggle(move |ix, on, _, _| toggles.borrow_mut().push((ix, on)))
.on_open(move |ix, _, _| opens.borrow_mut().push(ix)),
)
}
}
#[gpui::test]
fn a_breakpoints_box_toggles_without_opening_it(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (toggles, opens) = (
Rc::new(RefCell::new(Vec::new())),
Rc::new(RefCell::new(Vec::new())),
);
let view = Points {
toggles: toggles.clone(),
opens: opens.clone(),
};
let (_, cx) = cx.add_window_view(|_, _| view);
settle(cx);
cx.simulate_click(point(px(14.0), px(12.0)), Modifiers::none());
settle(cx);
assert_eq!(*toggles.borrow(), [(0, false)]);
assert!(
opens.borrow().is_empty(),
"the box press stays with the box"
);
cx.simulate_click(point(px(200.0), px(12.0)), Modifiers::none());
settle(cx);
assert_eq!(*opens.borrow(), [0], "the row still opens");
}
+13 -2
View File
@@ -10,7 +10,7 @@ use super::OnIndex;
use crate::{ use crate::{
charts::nice_step, charts::nice_step,
theme::{ActiveTheme, ControlSize, Radius, TextSize}, theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::{Ellipsis, LEADING}, typography::{Ellipsis, LEADING, format::decimals},
}; };
/// A stretch of work on a track, in milliseconds. /// A stretch of work on a track, in milliseconds.
@@ -25,6 +25,11 @@ pub struct Span {
/// Wheel pixels that zoom by a factor of e. /// Wheel pixels that zoom by a factor of e.
const ZOOM: f64 = 400.0; const ZOOM: f64 = 400.0;
/// A tick's time, in as many places as its step needs.
fn tick_label(time: f64, step: f64) -> String {
format!("{time:.*} ms", decimals(step))
}
/// Ticks along the ruler, about. /// Ticks along the ruler, about.
const TICKS: usize = 6; const TICKS: usize = 6;
@@ -147,7 +152,7 @@ impl RenderOnce for TimelineProfiler {
.border_l_1() .border_l_1()
.border_color(colors.border) .border_color(colors.border)
.text_color(colors.fg_subtle) .text_color(colors.fg_subtle)
.child(format!("{time:.0} ms")) .child(tick_label(*time, step))
})), })),
), ),
) )
@@ -267,6 +272,12 @@ impl RenderOnce for TimelineProfiler {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
fn ticks_keep_the_places_their_step_needs() {
assert_eq!(super::tick_label(0.2, 0.2), "0.2 ms");
assert_eq!(super::tick_label(15.0, 5.0), "15 ms");
}
use super::*; use super::*;
#[test] #[test]