T20: Debug

- DebugToolbar, BreakpointList, CallStack with folded library frames, ThreadList
- VariablesPanel and WatchPanel, colored by kind, changes washed
- HexViewer at a base address with the byte under the pointer lit on both sides
- Disassembly with source lines, registers and numbers colored, gutter breakpoints
- Flamegraph that zooms by frame, TimelineProfiler that zooms and pans by wheel
- NetworkInspector with a waterfall, a filter and a request's headers and body
- icons: redo-dot, arrow-down-to-dot, arrow-up-from-dot
- gallery: the Debug page
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 02:38:50 -04:00
1 parent 74dfb5130e
commit 74d6bd1771
22 files changed
+3240 -14

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
use std::{cell::Cell, rc::Rc};
use gpui::{
App, Bounds, ElementId, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, RenderOnce, ScrollWheelEvent, SharedString, StatefulInteractiveElement,
Styled, Window, canvas, div, prelude::*, relative,
};
use super::OnIndex;
use crate::{
charts::nice_step,
theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::{Ellipsis, LEADING},
};
/// A stretch of work on a track, in milliseconds.
#[derive(Clone, Debug, PartialEq)]
pub struct Span {
pub track: usize,
pub name: SharedString,
pub start: f64,
pub end: f64,
}
/// Wheel pixels that zoom by a factor of e.
const ZOOM: f64 = 400.0;
/// Ticks along the ruler, about.
const TICKS: usize = 6;
/// `range` scaled by `factor` around the point at `at` across it, from 0 to 1; above 1 widens, below narrows.
pub fn zoomed(range: (f64, f64), at: f64, factor: f64) -> (f64, f64) {
assert!(
factor > 0.0 && range.1 > range.0,
"a range zooms by a positive factor"
);
let pivot = range.0 + (range.1 - range.0) * at.clamp(0.0, 1.0);
(
pivot - (pivot - range.0) * factor,
pivot + (range.1 - pivot) * factor,
)
}
/// A steady color for a span's name.
fn tint(name: &str, palette: &[Hsla; 8]) -> Hsla {
let hash = name.bytes().fold(0u32, |hash, byte| {
hash.wrapping_mul(31).wrapping_add(byte as u32)
});
palette[hash as usize % palette.len()]
}
type OnRange = Rc<dyn Fn((f64, f64), &mut Window, &mut App)>;
/// Work over time on tracks, such as threads: each span a bar from its start to its end under a time axis. The wheel zooms around the pointer and a sideways wheel pans; a press picks a span.
#[derive(IntoElement)]
pub struct TimelineProfiler {
id: ElementId,
tracks: Vec<SharedString>,
spans: Rc<Vec<Span>>,
range: (f64, f64),
selected: Option<usize>,
on_range: Option<OnRange>,
on_select: Option<OnIndex>,
}
impl TimelineProfiler {
/// `range` is the stretch of time in view, in milliseconds.
pub fn new(
id: impl Into<ElementId>,
tracks: impl IntoIterator<Item = impl Into<SharedString>>,
spans: impl Into<Rc<Vec<Span>>>,
range: (f64, f64),
) -> Self {
Self {
id: id.into(),
tracks: tracks.into_iter().map(Into::into).collect(),
spans: spans.into(),
range,
selected: None,
on_range: None,
on_select: None,
}
}
pub fn selected(mut self, span: usize) -> Self {
self.selected = Some(span);
self
}
pub fn on_range(
mut self,
handler: impl Fn((f64, f64), &mut Window, &mut App) + 'static,
) -> Self {
self.on_range = Some(Rc::new(handler));
self
}
pub fn on_select(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
}
impl RenderOnce for TimelineProfiler {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let line = theme
.control_height(ControlSize::Md)
.to_pixels(window.rem_size());
let (from, to) = self.range;
let length = to - from;
let at = move |time: f64| ((time - from) / length) as f32;
let step = nice_step(length, TICKS);
let first = (from / step).ceil() as i64;
let ticks: Vec<f64> = (first..)
.map(|n| n as f64 * step)
.take_while(|time| *time < to)
.collect();
let lane = Rc::new(Cell::new(Bounds::<Pixels>::default()));
let (measure, wheel_lane) = (lane.clone(), lane);
let on_range = self.on_range.clone();
let label = theme.label_width() * 0.6;
let row = theme.control_height(ControlSize::Md);
let selected = self.selected.and_then(|ix| self.spans.get(ix).cloned());
div()
.flex()
.flex_col()
.gap_2()
.text_size(theme.text_size(TextSize::Xs))
.child(
div().flex().child(div().flex_none().w(label)).child(
div()
.relative()
.flex_1()
.h(theme.text_size(TextSize::Xs) * LEADING)
.line_height(relative(LEADING))
.overflow_hidden()
.border_b_1()
.border_color(colors.border)
.children(ticks.iter().map(|time| {
div()
.absolute()
.left(relative(at(*time)))
.top_0()
.pl_1()
.border_l_1()
.border_color(colors.border)
.text_color(colors.fg_subtle)
.child(format!("{time:.0} ms"))
})),
),
)
.child(
div()
.id(self.id.clone())
.flex()
.flex_col()
.gap_px()
.on_scroll_wheel(move |event: &ScrollWheelEvent, window, cx| {
let Some(on_range) = &on_range else {
return;
};
let bounds = wheel_lane.get();
let delta = event.delta.pixel_delta(line);
let (x, y) = (f64::from(f32::from(delta.x)), f64::from(f32::from(delta.y)));
cx.stop_propagation();
let width = f64::from(f32::from(bounds.size.width)).max(1.0);
if x.abs() > y.abs() {
let shift = -x / width * length;
on_range((from + shift, to + shift), window, cx);
} else {
let pointer =
f64::from(f32::from(event.position.x - bounds.origin.x)) / width;
on_range(zoomed((from, to), pointer, (-y / ZOOM).exp()), window, cx);
}
})
.children(self.tracks.iter().enumerate().map(|(track, name)| {
let bars = self
.spans
.iter()
.enumerate()
.filter(|(_, span)| {
span.track == track && span.end > from && span.start < to
})
.map(|(ix, span)| {
let (left, right) =
(at(span.start).max(0.0), at(span.end).min(1.0));
let lit = self.selected == Some(ix);
let pick = self.on_select.clone();
let color = tint(&span.name, &colors.chart);
div()
.id((self.id.clone(), format!("span-{ix}")))
.absolute()
.top_0p5()
.bottom_0p5()
.left(relative(left))
.w(relative(right - left))
.px_1()
.overflow_hidden()
.whitespace_nowrap()
.rounded(theme.radius(Radius::Sm))
.bg(if lit { color } else { color.opacity(0.55) })
.border_1()
.border_color(if lit {
colors.focus
} else {
color.opacity(0.0)
})
.text_color(colors.fg)
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default()
})
.when_some(pick, |bar, pick| {
bar.on_click(move |_, window, cx| pick(ix, window, cx))
})
.child(Ellipsis::new(span.name.clone()))
});
div()
.flex()
.h(row)
.child(
div()
.flex_none()
.w(label)
.pr_2()
.text_color(colors.fg_muted)
.font_weight(FontWeight::MEDIUM)
.child(name.clone()),
)
.child(
div()
.relative()
.flex_1()
.h_full()
.overflow_hidden()
.bg(colors.hover.opacity(0.5))
.when(track == 0, |lane| {
let measure = measure.clone();
lane.child(
canvas(
move |bounds, _, _| measure.set(bounds),
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
})
.children(bars),
)
})),
)
.children(selected.map(|span| {
div().text_color(colors.fg_muted).child(format!(
"{} · {:.1} ms · from {:.1} to {:.1} ms",
span.name,
span.end - span.start,
span.start,
span.end
))
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zoom_keeps_the_point_under_the_pointer() {
assert_eq!(zoomed((0.0, 100.0), 0.5, 0.5), (25.0, 75.0));
assert_eq!(
zoomed((0.0, 100.0), 0.0, 0.5),
(0.0, 50.0),
"zooming at the start keeps the start"
);
assert_eq!(zoomed((10.0, 20.0), 1.0, 2.0), (0.0, 20.0));
}
}