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 2c961833ac
commit f8bfa29c17
22 files changed
+3240 -14

No files matched your search

+265
View File
@@ -0,0 +1,265 @@
use std::rc::Rc;
use gpui::{
AnyElement, App, Div, ElementId, FontWeight, Hsla, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
prelude::*, relative,
};
use super::OnIndex;
use crate::{
theme::{ActiveTheme, Palette, Radius, TextSize},
typography::{Ellipsis, format, tabular},
};
/// A request the page made: method and address, status once answered, kind, size, when it started and how long it took in milliseconds, and its headers and body.
#[derive(Clone, Debug, PartialEq)]
pub struct Request {
pub method: SharedString,
pub url: SharedString,
pub status: Option<u16>,
pub kind: SharedString,
pub size: u64,
pub start: f64,
pub duration: f64,
pub headers: Vec<(SharedString, SharedString)>,
pub body: Option<SharedString>,
}
/// A status's color: quiet for success, then info, warning and danger.
fn status_color(status: Option<u16>, colors: &Palette) -> Hsla {
match status {
None => colors.fg_subtle,
Some(200..=299) => colors.fg,
Some(300..=399) => colors.info,
Some(400..=499) => colors.warning,
Some(_) => colors.danger,
}
}
/// The last part of an address's path, else its host.
pub(crate) fn name(url: &str) -> &str {
let bare = url.split(['?', '#']).next().unwrap_or(url);
let path = bare.split_once("://").map_or(bare, |(_, rest)| rest);
path.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(path)
}
/// Requests in the order they started: name, status, kind, size and time, with a waterfall of when each ran; a query keeps those whose address holds it. A press opens a request's headers and body below.
#[derive(IntoElement)]
pub struct NetworkInspector {
id: ElementId,
requests: Vec<Request>,
query: SharedString,
selected: Option<usize>,
on_select: Option<OnIndex>,
}
impl NetworkInspector {
pub fn new(id: impl Into<ElementId>, requests: impl IntoIterator<Item = Request>) -> Self {
Self {
id: id.into(),
requests: requests.into_iter().collect(),
query: SharedString::default(),
selected: None,
on_select: None,
}
}
pub fn query(mut self, query: impl Into<SharedString>) -> Self {
self.query = query.into();
self
}
pub fn selected(mut self, request: usize) -> Self {
self.selected = Some(request);
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 NetworkInspector {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let query = self.query.to_lowercase();
let end = self
.requests
.iter()
.map(|request| request.start + request.duration)
.fold(0.0_f64, f64::max)
.max(1.0);
let columns = |row: Div| row.flex().items_center().gap_3().px_2();
let widths = [0.5, 0.6, 0.6, 0.6, 2.0].map(|share| theme.label_width() * share);
let head = columns(div())
.py_1()
.border_b_1()
.border_color(colors.border)
.text_color(colors.fg_subtle)
.child(div().flex_1().min_w_0().child("Name"))
.children(
["Status", "Type", "Size", "Time", "Waterfall"]
.iter()
.zip(widths)
.map(|(title, width)| div().flex_none().w(width).child(*title)),
);
let rows: Vec<AnyElement> = self
.requests
.iter()
.enumerate()
.filter(|(_, request)| query.is_empty() || request.url.to_lowercase().contains(&query))
.map(|(ix, request)| {
let lit = self.selected == Some(ix);
let pick = self.on_select.clone();
let tint = status_color(request.status, &colors);
columns(div())
.id((self.id.clone(), format!("request-{ix}")))
.py_1()
.rounded(theme.radius(Radius::Sm))
.cursor_pointer()
.when(lit, |row| row.bg(colors.active))
.when(!lit, |row| row.hover(|row| row.bg(colors.hover)))
.when_some(pick, |row, pick| {
row.on_click(move |_, window, cx| pick(ix, window, cx))
})
.child(
div()
.flex_1()
.min_w_0()
.text_color(if request.status.is_some_and(|code| code >= 400) {
tint
} else {
colors.fg
})
.child(Ellipsis::new(name(&request.url).to_string())),
)
.child(
tabular(div().flex_none().w(widths[0]).text_color(tint)).child(
request
.status
.map_or("—".to_string(), |code| code.to_string()),
),
)
.child(
div()
.flex_none()
.w(widths[1])
.text_color(colors.fg_muted)
.child(request.kind.clone()),
)
.child(
tabular(div().flex_none().w(widths[2]).text_color(colors.fg_muted))
.child(format::file_size(request.size, false)),
)
.child(
tabular(div().flex_none().w(widths[3]).text_color(colors.fg_muted))
.child(format!("{:.0} ms", request.duration)),
)
.child(
div().flex_none().w(widths[4]).h_2().relative().child(
div()
.absolute()
.top_0()
.bottom_0()
.left(relative((request.start / end) as f32))
.w(relative(((request.duration / end) as f32).max(0.005)))
.rounded(theme.radius(Radius::Sm))
.bg(if request.status.is_none() {
colors.fg_subtle
} else {
colors.chart[0]
}),
),
)
.into_any_element()
})
.collect();
let details = self
.selected
.and_then(|ix| self.requests.get(ix))
.map(|request| {
let pair = |key: SharedString, value: SharedString| {
div()
.flex()
.gap_3()
.child(
div()
.flex_none()
.w(theme.label_width())
.text_color(colors.fg_subtle)
.child(key),
)
.child(
div()
.flex_1()
.min_w_0()
.font_family(theme.mono_family.clone())
.text_color(colors.fg)
.child(value),
)
};
div()
.flex()
.flex_col()
.gap_1()
.p_3()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(colors.border)
.child(
div()
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg)
.child(format!("{} {}", request.method, request.url)),
)
.child(pair(
"Status".into(),
request
.status
.map_or("pending".into(), |code| code.to_string().into()),
))
.children(
request
.headers
.iter()
.map(|(key, value)| pair(key.clone(), value.clone())),
)
.children(request.body.clone().map(|body| {
div()
.mt_2()
.p_2()
.rounded(theme.radius(Radius::Sm))
.bg(colors.sunken)
.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg)
.child(body)
}))
});
div()
.flex()
.flex_col()
.gap_2()
.text_size(theme.text_size(TextSize::Sm))
.child(div().flex().flex_col().child(head).children(rows))
.children(details)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_request_goes_by_the_last_part_of_its_path() {
assert_eq!(name("https://api.ely.dev/v1/quotes?symbol=ELY"), "quotes");
assert_eq!(name("https://ely.dev/"), "ely.dev");
assert_eq!(name("/static/app.js#top"), "app.js");
}
}