diff --git a/TASKS.md b/TASKS.md index 05d271a..9bdb6bf 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 | 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 | | -| 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 | diff --git a/examples/gallery/pages/debug/machine.rs b/examples/gallery/pages/debug/machine.rs index 4b686db..e450250 100644 --- a/examples/gallery/pages/debug/machine.rs +++ b/examples/gallery/pages/debug/machine.rs @@ -141,7 +141,7 @@ pub fn machine(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { frame().h(px(220.)).child( HexViewer::new("debug-hex", memory()) .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)), ), ) diff --git a/examples/gallery/pages/debug/session.rs b/examples/gallery/pages/debug/session.rs index 0c95ccb..f6daca6 100644 --- a/examples/gallery/pages/debug/session.rs +++ b/examples/gallery/pages/debug/session.rs @@ -106,7 +106,7 @@ pub fn session(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let (command, tell) = (state.clone(), said.clone()); section( "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, ) .child(row().child(DebugToolbar::new("debug-toolbar", now_state).on_command(move |which, _, cx| { diff --git a/src/buttons/mod.rs b/src/buttons/mod.rs index f04189d..312c257 100644 --- a/src/buttons/mod.rs +++ b/src/buttons/mod.rs @@ -15,6 +15,7 @@ mod sheet; mod tests; pub use bars::{ActionBar, BulkActionBar}; +pub(crate) use button::shortcut_text; pub use button::{Button, ButtonVariant}; pub use confirm::{ConfirmButton, ConfirmMode}; pub use copy::CopyButton; diff --git a/src/debug/disasm.rs b/src/debug/disasm.rs index f32a999..ab31908 100644 --- a/src/debug/disasm.rs +++ b/src/debug/disasm.rs @@ -185,7 +185,6 @@ impl RenderOnce for Disassembly { .id(row) .w_full() .flex() - .items_center() .gap_3() .whitespace_nowrap() .when(is_current, |line| line.bg(colors.warning.opacity(0.12))) @@ -195,6 +194,7 @@ impl RenderOnce for Disassembly { .flex_none() .w_5() .flex() + .items_center() .justify_center() .cursor_pointer() .on_mouse_down(MouseButton::Left, |_, window, _| { diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 6f1083a..673d0f1 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -8,6 +8,8 @@ mod hex; mod network; mod session; mod stack; +#[cfg(all(test, feature = "test-support"))] +mod tests; mod timeline; mod variables; diff --git a/src/debug/session.rs b/src/debug/session.rs index e906d5f..d56016b 100644 --- a/src/debug/session.rs +++ b/src/debug/session.rs @@ -1,13 +1,14 @@ use std::rc::Rc; use gpui::{ - AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, - RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, + AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, Keystroke, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, + prelude::*, }; use super::OnIndex; use crate::{ - buttons::{ButtonVariant, IconButton}, + buttons::{ButtonVariant, IconButton, shortcut_text}, forms::Checkbox, primitives::IconName, theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize}, @@ -36,7 +37,7 @@ pub enum DebugCommand { type OnCommand = Rc; type OnSwitch = Rc; -/// 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)] pub struct DebugToolbar { id: ElementId, @@ -67,20 +68,22 @@ impl RenderOnce for DebugToolbar { let theme = cx.theme(); let colors = theme.colors.clone(); let paused = self.state == DebugState::Paused; - let button = |command: DebugCommand, icon: IconName, words: &'static str, enabled: bool| { - let run = self.on_command.clone(); - IconButton::new((self.id.clone(), format!("{command:?}")), icon) - .variant(ButtonVariant::Ghost) - .size(ControlSize::Sm) - .tooltip(words) - .disabled(!enabled) - .when_some(run, |button, run| { - button.on_click(move |_, window, cx| { - log::info!("debug toolbar: {command:?}"); - run(command, window, cx) + let button = + |command: DebugCommand, icon: IconName, words: &str, key: &str, enabled: bool| { + let run = self.on_command.clone(); + let key = Keystroke::parse(key).expect("a debug key parses"); + IconButton::new((self.id.clone(), format!("{command:?}")), icon) + .variant(ButtonVariant::Ghost) + .size(ControlSize::Sm) + .tooltip(format!("{words} {}", shortcut_text(&key))) + .disabled(!enabled) + .when_some(run, |button, run| { + 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); div() .flex() @@ -93,39 +96,50 @@ impl RenderOnce for DebugToolbar { .bg(colors.overlay) .shadow(theme.elevation(Elevation::Floating)) .child(if paused { - button(DebugCommand::Continue, IconName::Play, "Continue F5", true) + button( + DebugCommand::Continue, + IconName::Play, + "Continue", + "f5", + true, + ) } else { - button(DebugCommand::Pause, IconName::Pause, "Pause F6", true) + button(DebugCommand::Pause, IconName::Pause, "Pause", "f6", true) }) .child(button( DebugCommand::StepOver, IconName::RedoDot, - "Step over F10", + "Step over", + "f10", paused, )) .child(button( DebugCommand::StepInto, IconName::ArrowDownToDot, - "Step into F11", + "Step into", + "f11", paused, )) .child(button( DebugCommand::StepOut, IconName::ArrowUpFromDot, - "Step out ⇧F11", + "Step out", + "shift-f11", paused, )) .child(rule()) .child(button( DebugCommand::Restart, IconName::RotateCw, - "Restart ⇧⌘F5", + "Restart", + "secondary-shift-f5", true, )) .child(button( DebugCommand::Stop, IconName::Square, - "Stop ⇧F5", + "Stop", + "shift-f5", true, )) } @@ -142,7 +156,7 @@ pub struct Breakpoint { 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)] pub struct BreakpointList { id: ElementId, @@ -200,9 +214,10 @@ impl RenderOnce for BreakpointList { 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 = match toggle { - Some(toggle) => { - check.on_change(move |on, window, cx| toggle(ix, on, window, cx)) - } + Some(toggle) => check.on_change(move |on, window, cx| { + cx.stop_propagation(); + toggle(ix, on, window, cx) + }), None => check, }; div() diff --git a/src/debug/tests.rs b/src/debug/tests.rs new file mode 100644 index 0000000..422b2db --- /dev/null +++ b/src/debug/tests.rs @@ -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>>); + +impl Render for Listing { + fn render(&mut self, _: &mut Window, _: &mut Context) -> 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>>, + opens: Rc>>, +} + +impl Render for Points { + fn render(&mut self, _: &mut Window, _: &mut Context) -> 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"); +} diff --git a/src/debug/timeline.rs b/src/debug/timeline.rs index 15ce23e..da87786 100644 --- a/src/debug/timeline.rs +++ b/src/debug/timeline.rs @@ -10,7 +10,7 @@ use super::OnIndex; use crate::{ charts::nice_step, theme::{ActiveTheme, ControlSize, Radius, TextSize}, - typography::{Ellipsis, LEADING}, + typography::{Ellipsis, LEADING, format::decimals}, }; /// 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. 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. const TICKS: usize = 6; @@ -147,7 +152,7 @@ impl RenderOnce for TimelineProfiler { .border_l_1() .border_color(colors.border) .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)] 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::*; #[test]