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:
1 parent
2c961833ac
commit
f8bfa29c17
22 files changed
+3240
-14
No files matched your search
@@ -0,0 +1,303 @@
|
||||
use std::{ops::Range, rc::Rc, sync::LazyLock};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, HighlightStyle, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div,
|
||||
prelude::*, uniform_list,
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
};
|
||||
|
||||
/// An instruction: its address, its bytes, its mnemonic and operands, an optional comment, and the source line it starts, when known.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Instruction {
|
||||
pub address: u64,
|
||||
pub bytes: Vec<u8>,
|
||||
pub mnemonic: SharedString,
|
||||
pub operands: SharedString,
|
||||
pub comment: Option<SharedString>,
|
||||
pub source: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// How a piece of an operand reads.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Piece {
|
||||
Register,
|
||||
Number,
|
||||
Mark,
|
||||
}
|
||||
|
||||
static PIECES: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?x)
|
||||
(?P<register>\b(?:[re]?(?:ax|bx|cx|dx|si|di|sp|bp|ip)|r(?:[89]|1[0-5])[dwb]?|[xyz]mm[0-9]{1,2}|[abcd][lh]|[xw](?:[12]?[0-9]|30)|sp|lr|fp|pc|[xw]zr|[vqdsbh][0-9]{1,2})\b)
|
||||
|(?P<number>\#?-?(?:0x[0-9a-fA-F]+|\b[0-9]+\b))
|
||||
|(?P<mark>[\[\],+*!\-\#])",
|
||||
)
|
||||
.expect("the operand pattern compiles")
|
||||
});
|
||||
|
||||
/// Registers, numbers and marks in `operands`, in order.
|
||||
pub(crate) fn pieces(operands: &str) -> Vec<(Range<usize>, Piece)> {
|
||||
PIECES
|
||||
.captures_iter(operands)
|
||||
.map(|found| {
|
||||
let (kind, hit) = if let Some(hit) = found.name("register") {
|
||||
(Piece::Register, hit)
|
||||
} else if let Some(hit) = found.name("number") {
|
||||
(Piece::Number, hit)
|
||||
} else {
|
||||
(
|
||||
Piece::Mark,
|
||||
found.name("mark").expect("a piece is one of three"),
|
||||
)
|
||||
};
|
||||
(hit.range(), kind)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A row: an instruction, or the source line above one.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Line {
|
||||
Source(usize),
|
||||
Code(usize),
|
||||
}
|
||||
|
||||
/// Machine code as read: address, bytes, the mnemonic and its operands colored, comments, and the source lines they came from. The current instruction is marked; a press in the gutter sets or clears a breakpoint. Long listings draw only the rows in view; it fills its box.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disassembly {
|
||||
id: ElementId,
|
||||
instructions: Rc<Vec<Instruction>>,
|
||||
current: Option<usize>,
|
||||
breakpoints: Vec<usize>,
|
||||
on_breakpoint: Option<OnIndex>,
|
||||
}
|
||||
|
||||
impl Disassembly {
|
||||
pub fn new(id: impl Into<ElementId>, instructions: impl Into<Rc<Vec<Instruction>>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
instructions: instructions.into(),
|
||||
current: None,
|
||||
breakpoints: Vec::new(),
|
||||
on_breakpoint: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(mut self, instruction: usize) -> Self {
|
||||
self.current = Some(instruction);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn breakpoints(mut self, instructions: impl IntoIterator<Item = usize>) -> Self {
|
||||
self.breakpoints = instructions.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_breakpoint(
|
||||
mut self,
|
||||
handler: impl Fn(usize, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_breakpoint = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Disassembly {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let lines: Rc<Vec<Line>> = Rc::new(
|
||||
self.instructions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(ix, instruction)| {
|
||||
instruction
|
||||
.source
|
||||
.as_ref()
|
||||
.map(|_| Line::Source(ix))
|
||||
.into_iter()
|
||||
.chain([Line::Code(ix)])
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let widest = self
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| instruction.bytes.len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let (instructions, current, breakpoints, on_breakpoint, id) = (
|
||||
self.instructions.clone(),
|
||||
self.current,
|
||||
self.breakpoints.clone(),
|
||||
self.on_breakpoint.clone(),
|
||||
self.id.clone(),
|
||||
);
|
||||
let list = uniform_list(
|
||||
(self.id.clone(), "rows"),
|
||||
lines.len(),
|
||||
move |range, _, cx| {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let syntax = colors.syntax;
|
||||
range
|
||||
.map(|row| match lines[row] {
|
||||
Line::Source(ix) => div()
|
||||
.id(row)
|
||||
.w_full()
|
||||
.pl_8()
|
||||
.whitespace_nowrap()
|
||||
.text_color(syntax.comment)
|
||||
.child(
|
||||
instructions[ix]
|
||||
.source
|
||||
.clone()
|
||||
.expect("a source row has its line"),
|
||||
),
|
||||
Line::Code(ix) => {
|
||||
let instruction = &instructions[ix];
|
||||
let styles: Vec<(Range<usize>, HighlightStyle)> =
|
||||
pieces(&instruction.operands)
|
||||
.into_iter()
|
||||
.map(|(range, piece)| {
|
||||
let color = match piece {
|
||||
Piece::Register => syntax.variable,
|
||||
Piece::Number => syntax.number,
|
||||
Piece::Mark => syntax.punctuation,
|
||||
};
|
||||
(range, color.into())
|
||||
})
|
||||
.collect();
|
||||
let (marked, is_current) =
|
||||
(breakpoints.contains(&ix), current == Some(ix));
|
||||
let toggle = on_breakpoint.clone();
|
||||
let hex: Vec<String> = instruction
|
||||
.bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect();
|
||||
div()
|
||||
.id(row)
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.whitespace_nowrap()
|
||||
.when(is_current, |line| line.bg(colors.warning.opacity(0.12)))
|
||||
.child(
|
||||
div()
|
||||
.id((id.clone(), format!("gutter-{ix}")))
|
||||
.flex_none()
|
||||
.w_5()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| {
|
||||
window.prevent_default()
|
||||
})
|
||||
.when_some(toggle, |gutter, toggle| {
|
||||
gutter.on_click(move |_, window, cx| {
|
||||
toggle(ix, window, cx)
|
||||
})
|
||||
})
|
||||
.map(|gutter| {
|
||||
if is_current {
|
||||
gutter.child(
|
||||
Icon::new(IconName::ArrowRight)
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.warning),
|
||||
)
|
||||
} else if marked {
|
||||
gutter.child(
|
||||
div().size_2().rounded_full().bg(colors.danger),
|
||||
)
|
||||
} else {
|
||||
gutter
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(format!("{:#010x}", instruction.address)),
|
||||
)
|
||||
.child(div().flex_none().text_color(colors.fg_subtle).child(
|
||||
format!("{:<width$}", hex.join(" "), width = widest * 3),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w_12()
|
||||
.text_color(syntax.keyword)
|
||||
.child(instruction.mnemonic.clone()),
|
||||
)
|
||||
.child(
|
||||
StyledText::new(instruction.operands.clone())
|
||||
.with_highlights(styles),
|
||||
)
|
||||
.children(instruction.comment.clone().map(|comment| {
|
||||
div()
|
||||
.text_color(syntax.comment)
|
||||
.child(format!("; {comment}"))
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.size_full();
|
||||
div()
|
||||
.size_full()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg)
|
||||
.child(list)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn kinds(operands: &str) -> Vec<(&str, Piece)> {
|
||||
pieces(operands)
|
||||
.into_iter()
|
||||
.map(|(range, piece)| (&operands[range], piece))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operands_split_into_registers_numbers_and_marks() {
|
||||
use Piece::*;
|
||||
assert_eq!(
|
||||
kinds("qword ptr [rbp - 0x18], rax"),
|
||||
[
|
||||
("[", Mark),
|
||||
("rbp", Register),
|
||||
("-", Mark),
|
||||
("0x18", Number),
|
||||
("]", Mark),
|
||||
(",", Mark),
|
||||
("rax", Register)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
kinds("x0, [sp, #16]"),
|
||||
[
|
||||
("x0", Register),
|
||||
(",", Mark),
|
||||
("[", Mark),
|
||||
("sp", Register),
|
||||
(",", Mark),
|
||||
("#16", Number),
|
||||
("]", Mark)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, ParentElement, Pixels, RenderOnce, SharedString, Styled, Window, canvas, div,
|
||||
fill, point, size,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
typography::format,
|
||||
};
|
||||
|
||||
/// A frame of a profile: its function, the samples taken in it alone, and the frames it called.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ProfileFrame {
|
||||
pub name: SharedString,
|
||||
pub own: u64,
|
||||
pub calls: Vec<ProfileFrame>,
|
||||
}
|
||||
|
||||
impl ProfileFrame {
|
||||
/// Samples in this frame and every frame under it.
|
||||
pub fn total(&self) -> u64 {
|
||||
self.own + self.calls.iter().map(ProfileFrame::total).sum::<u64>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame as the graph lays it out: its path of call indices from the root, its depth, and its share of the width.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Placed {
|
||||
pub path: Vec<usize>,
|
||||
pub name: SharedString,
|
||||
pub depth: usize,
|
||||
pub start: f32,
|
||||
pub end: f32,
|
||||
pub samples: u64,
|
||||
}
|
||||
|
||||
/// The frames under `focus`, a path of call indices, each spanning its share of the samples; `focus` fills the width and its callers stack above it, full width.
|
||||
pub fn place(root: &ProfileFrame, focus: &[usize]) -> Vec<Placed> {
|
||||
let mut out = Vec::new();
|
||||
let mut frame = root;
|
||||
for (depth, ix) in focus.iter().enumerate() {
|
||||
out.push(Placed {
|
||||
path: focus[..depth].to_vec(),
|
||||
name: frame.name.clone(),
|
||||
depth,
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
samples: frame.total(),
|
||||
});
|
||||
frame = &frame.calls[*ix];
|
||||
}
|
||||
fn under(
|
||||
frame: &ProfileFrame,
|
||||
path: Vec<usize>,
|
||||
depth: usize,
|
||||
start: f32,
|
||||
end: f32,
|
||||
out: &mut Vec<Placed>,
|
||||
) {
|
||||
let total = frame.total().max(1) as f32;
|
||||
out.push(Placed {
|
||||
path: path.clone(),
|
||||
name: frame.name.clone(),
|
||||
depth,
|
||||
start,
|
||||
end,
|
||||
samples: frame.total(),
|
||||
});
|
||||
let mut at = start;
|
||||
for (ix, call) in frame.calls.iter().enumerate() {
|
||||
let width = (end - start) * call.total() as f32 / total;
|
||||
let mut inner = path.clone();
|
||||
inner.push(ix);
|
||||
under(call, inner, depth + 1, at, at + width, out);
|
||||
at += width;
|
||||
}
|
||||
}
|
||||
under(frame, focus.to_vec(), focus.len(), 0.0, 1.0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
type OnFocus = Rc<dyn Fn(Vec<usize>, &mut Window, &mut App)>;
|
||||
type OnPath = Rc<dyn Fn(Option<Vec<usize>>, &mut Window, &mut App)>;
|
||||
|
||||
/// A flame graph: each function as wide as its samples, callees stacked under callers, colored by name in the chart palette. Hover reads a frame; a press zooms into it and a press on a caller above zooms back out.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Flamegraph {
|
||||
id: ElementId,
|
||||
root: Rc<ProfileFrame>,
|
||||
focus: Vec<usize>,
|
||||
hovered: Option<Vec<usize>>,
|
||||
on_focus: Option<OnFocus>,
|
||||
on_hover: Option<OnPath>,
|
||||
}
|
||||
|
||||
impl Flamegraph {
|
||||
pub fn new(id: impl Into<ElementId>, root: impl Into<Rc<ProfileFrame>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
root: root.into(),
|
||||
focus: Vec::new(),
|
||||
hovered: None,
|
||||
on_focus: None,
|
||||
on_hover: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame zoomed into, as call indices from the root.
|
||||
pub fn focus(mut self, path: Vec<usize>) -> Self {
|
||||
self.focus = path;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hovered(mut self, path: Option<Vec<usize>>) -> Self {
|
||||
self.hovered = path;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_focus(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<usize>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_focus = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(
|
||||
mut self,
|
||||
handler: impl Fn(Option<Vec<usize>>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_hover = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A name cut to `room` characters, ending in an ellipsis; nothing when fewer than two fit.
|
||||
fn fitted(name: &str, room: usize) -> String {
|
||||
let count = name.chars().count();
|
||||
match room {
|
||||
room if room >= count => name.to_string(),
|
||||
room if room < 2 => String::new(),
|
||||
room => name.chars().take(room - 1).chain(['…']).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A steady color for a function'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()]
|
||||
}
|
||||
|
||||
impl RenderOnce for Flamegraph {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let row = theme
|
||||
.control_height(ControlSize::Sm)
|
||||
.to_pixels(window.rem_size());
|
||||
let placed = Rc::new(place(&self.root, &self.focus));
|
||||
let depth = placed.iter().map(|frame| frame.depth).max().unwrap_or(0) + 1;
|
||||
let total = self.root.total();
|
||||
let reading = self
|
||||
.hovered
|
||||
.as_ref()
|
||||
.and_then(|path| placed.iter().find(|frame| &frame.path == path))
|
||||
.map(|frame| {
|
||||
format!(
|
||||
"{} · {} · {:.1} % of all",
|
||||
frame.name,
|
||||
format::plural(frame.samples, "sample", "samples"),
|
||||
frame.samples as f32 * 100.0 / total.max(1) as f32
|
||||
)
|
||||
});
|
||||
let hit = move |placed: &[Placed],
|
||||
bounds: Bounds<Pixels>,
|
||||
at: gpui::Point<Pixels>|
|
||||
-> Option<Vec<usize>> {
|
||||
let x = (at.x - bounds.origin.x) / bounds.size.width;
|
||||
let level = ((at.y - bounds.origin.y) / row).floor() as usize;
|
||||
placed
|
||||
.iter()
|
||||
.find(|frame| frame.depth == level && frame.start <= x && x < frame.end)
|
||||
.map(|frame| frame.path.clone())
|
||||
};
|
||||
let (paint_placed, press_placed, move_placed) =
|
||||
(placed.clone(), placed.clone(), placed.clone());
|
||||
let (hovered, palette) = (self.hovered.clone(), colors.chart);
|
||||
let (on_focus, on_hover) = (self.on_focus, self.on_hover);
|
||||
let (fg, hover_ring, text_size) = (colors.fg, colors.focus, theme.text_size(TextSize::Xs));
|
||||
let font = theme.mono_family.clone();
|
||||
let gap = theme.chart().hairline;
|
||||
let bounds_cell = Rc::new(std::cell::Cell::new(Bounds::default()));
|
||||
let (measure, pressed_at, moved_at) =
|
||||
(bounds_cell.clone(), bounds_cell.clone(), bounds_cell);
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.h(row)
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg_muted)
|
||||
.child(reading.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{} in all · press a frame to zoom in",
|
||||
format::plural(total, "sample", "samples")
|
||||
)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.relative()
|
||||
.w_full()
|
||||
.h(row * depth as f32)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
if let (Some(on_focus), Some(path)) = (
|
||||
&on_focus,
|
||||
hit(&press_placed, pressed_at.get(), event.position),
|
||||
) {
|
||||
log::info!("flamegraph: focus {path:?}");
|
||||
on_focus(path, window, cx)
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_mouse_move(move |event: &MouseMoveEvent, window, cx| {
|
||||
if let Some(on_hover) = &on_hover {
|
||||
on_hover(
|
||||
hit(&move_placed, moved_at.get(), event.position),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, _| measure.set(bounds),
|
||||
move |bounds, _, window, cx| {
|
||||
let pixels = text_size.to_pixels(window.rem_size());
|
||||
let face = gpui::font(font.clone());
|
||||
let advance = window
|
||||
.text_system()
|
||||
.advance(window.text_system().resolve_font(&face), pixels, 'm')
|
||||
.expect("the code font has an m")
|
||||
.width;
|
||||
for frame in paint_placed.iter() {
|
||||
let x0 = bounds.origin.x + bounds.size.width * frame.start;
|
||||
let width = bounds.size.width * (frame.end - frame.start);
|
||||
let origin =
|
||||
point(x0, bounds.origin.y + row * frame.depth as f32);
|
||||
let cell = Bounds::new(
|
||||
origin,
|
||||
size(width - gap.min(width), row - gap),
|
||||
);
|
||||
let color = tint(&frame.name, &palette);
|
||||
let lit = hovered.as_ref() == Some(&frame.path);
|
||||
window.paint_quad(fill(
|
||||
cell,
|
||||
if lit { color } else { color.opacity(0.55) },
|
||||
));
|
||||
if lit {
|
||||
window.paint_quad(gpui::outline(
|
||||
cell,
|
||||
hover_ring,
|
||||
gpui::BorderStyle::Solid,
|
||||
));
|
||||
}
|
||||
let label = fitted(
|
||||
&frame.name,
|
||||
((width - pixels * 0.8) / advance).max(0.0) as usize,
|
||||
);
|
||||
if !label.is_empty() {
|
||||
let run = gpui::TextRun {
|
||||
len: label.len(),
|
||||
font: face.clone(),
|
||||
color: fg,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(label.into(), pixels, &[run], None)
|
||||
.paint(
|
||||
origin
|
||||
+ point(
|
||||
pixels * 0.4,
|
||||
(row - pixels * 1.2) / 2.0,
|
||||
),
|
||||
pixels * 1.2,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.expect("a flame label paints");
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn frame(name: &str, own: u64, calls: Vec<ProfileFrame>) -> ProfileFrame {
|
||||
ProfileFrame {
|
||||
name: name.to_string().into(),
|
||||
own,
|
||||
calls,
|
||||
}
|
||||
}
|
||||
|
||||
fn profile() -> ProfileFrame {
|
||||
frame(
|
||||
"main",
|
||||
10,
|
||||
vec![
|
||||
frame("render", 30, vec![frame("layout", 20, vec![])]),
|
||||
frame("io", 40, vec![]),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_fit_their_frames() {
|
||||
assert_eq!(fitted("render", 10), "render");
|
||||
assert_eq!(fitted("render", 4), "ren…");
|
||||
assert_eq!(fitted("render", 1), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_span_their_share() {
|
||||
let placed = place(&profile(), &[]);
|
||||
let spans: Vec<(&str, usize, f32, f32)> = placed
|
||||
.iter()
|
||||
.map(|frame| (frame.name.as_ref(), frame.depth, frame.start, frame.end))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
spans,
|
||||
[
|
||||
("main", 0, 0.0, 1.0),
|
||||
("render", 1, 0.0, 0.5),
|
||||
("layout", 2, 0.0, 0.2),
|
||||
("io", 1, 0.5, 0.9)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_focused_frame_fills_the_width_under_its_callers() {
|
||||
let placed = place(&profile(), &[0]);
|
||||
let spans: Vec<(&str, usize, f32, f32)> = placed
|
||||
.iter()
|
||||
.map(|frame| (frame.name.as_ref(), frame.depth, frame.start, frame.end))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
spans,
|
||||
[
|
||||
("main", 0, 0.0, 1.0),
|
||||
("render", 1, 0.0, 1.0),
|
||||
("layout", 2, 0.0, 0.4)
|
||||
]
|
||||
);
|
||||
assert_eq!(placed[1].path, [0], "the focused frame keeps its path");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*, uniform_list,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::theme::{ActiveTheme, TextSize};
|
||||
|
||||
/// Bytes a row shows.
|
||||
const WIDTH: usize = 16;
|
||||
|
||||
/// How a byte reads as text: itself when it prints, a dot when not.
|
||||
fn glyph(byte: u8) -> char {
|
||||
if byte.is_ascii_graphic() || byte == b' ' {
|
||||
byte as char
|
||||
} else {
|
||||
'·'
|
||||
}
|
||||
}
|
||||
|
||||
/// What the bytes from `at` read as: the byte in hex, decimal, signed and binary, and the little-endian words that fit.
|
||||
pub(crate) fn inspect(bytes: &[u8], at: usize) -> String {
|
||||
let byte = bytes[at];
|
||||
let mut parts = vec![
|
||||
format!("0x{byte:02X}"),
|
||||
byte.to_string(),
|
||||
format!("i8 {}", byte as i8),
|
||||
format!("0b{byte:08b}"),
|
||||
];
|
||||
if let Some(word) = bytes.get(at..at + 2) {
|
||||
parts.push(format!("u16 {}", u16::from_le_bytes([word[0], word[1]])));
|
||||
}
|
||||
if let Some(word) = bytes.get(at..at + 4) {
|
||||
parts.push(format!(
|
||||
"u32 {}",
|
||||
u32::from_le_bytes([word[0], word[1], word[2], word[3]])
|
||||
));
|
||||
}
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
/// Memory as a hex dump: each row its address, sixteen bytes in fours, and the same bytes as text. A press selects a byte; the one under the pointer lights on both sides, and the selection reads below as numbers. Long dumps draw only the rows in view; it fills its box.
|
||||
#[derive(IntoElement)]
|
||||
pub struct HexViewer {
|
||||
id: ElementId,
|
||||
bytes: Rc<Vec<u8>>,
|
||||
base: u64,
|
||||
selected: Option<Range<usize>>,
|
||||
on_select: Option<OnIndex>,
|
||||
}
|
||||
|
||||
impl HexViewer {
|
||||
pub fn new(id: impl Into<ElementId>, bytes: impl Into<Rc<Vec<u8>>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
bytes: bytes.into(),
|
||||
base: 0,
|
||||
selected: None,
|
||||
on_select: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The address of the first byte.
|
||||
pub fn base(mut self, base: u64) -> Self {
|
||||
self.base = base;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, bytes: Range<usize>) -> Self {
|
||||
assert!(
|
||||
bytes.end <= self.bytes.len(),
|
||||
"a selection lies inside the bytes"
|
||||
);
|
||||
self.selected = Some(bytes);
|
||||
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 HexViewer {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let hovered =
|
||||
window.use_keyed_state((self.id.clone(), "hovered"), cx, |_, _| None::<usize>);
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let rows = self.bytes.len().div_ceil(WIDTH);
|
||||
let digits = format!("{:X}", self.base + self.bytes.len() as u64)
|
||||
.len()
|
||||
.max(8);
|
||||
let reading = self
|
||||
.selected
|
||||
.as_ref()
|
||||
.map(|range| inspect(&self.bytes, range.start));
|
||||
let (bytes, base, selected, on_select, id) = (
|
||||
self.bytes.clone(),
|
||||
self.base,
|
||||
self.selected.clone(),
|
||||
self.on_select.clone(),
|
||||
self.id.clone(),
|
||||
);
|
||||
let list = uniform_list((self.id.clone(), "rows"), rows, move |range, _, cx| {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let lit = *hovered.read(cx);
|
||||
range
|
||||
.map(|row| {
|
||||
let start = row * WIDTH;
|
||||
let end = (start + WIDTH).min(bytes.len());
|
||||
let wash = |at: usize| {
|
||||
if selected.as_ref().is_some_and(|range| range.contains(&at)) {
|
||||
Some(colors.selection)
|
||||
} else if lit == Some(at) {
|
||||
Some(colors.hover)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let cell = |at: usize, text: String, key: &str| {
|
||||
let (pick, hover) = (on_select.clone(), hovered.clone());
|
||||
div()
|
||||
.id((id.clone(), format!("{key}-{at}")))
|
||||
.px_0p5()
|
||||
.when_some(wash(at), |cell, wash| cell.bg(wash))
|
||||
.on_hover(move |on, _, cx| {
|
||||
hover.update(cx, |hovered, cx| {
|
||||
let next = if *on { Some(at) } else { None };
|
||||
if *hovered != next && (*on || *hovered == Some(at)) {
|
||||
*hovered = next;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| {
|
||||
window.prevent_default()
|
||||
})
|
||||
.when_some(pick, |cell, pick| {
|
||||
cell.on_click(move |_, window, cx| pick(at, window, cx))
|
||||
})
|
||||
.child(text)
|
||||
};
|
||||
div()
|
||||
.id(row)
|
||||
.w_full()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
div()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(format!("{:0digits$X}", base + start as u64)),
|
||||
)
|
||||
.child(div().flex().gap_2().children(
|
||||
(start..end).collect::<Vec<_>>().chunks(4).map(|four| {
|
||||
div().flex().children(
|
||||
four.iter()
|
||||
.map(|at| cell(*at, format!("{:02X}", bytes[*at]), "hex")),
|
||||
)
|
||||
}),
|
||||
))
|
||||
.child(div().flex().text_color(colors.fg_muted).children(
|
||||
(start..end).map(|at| cell(at, glyph(bytes[at]).to_string(), "text")),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.size_full();
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg)
|
||||
.child(list),
|
||||
)
|
||||
.children(reading.map(|reading| {
|
||||
div()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg_muted)
|
||||
.child(reading)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bytes_read_as_numbers_and_text() {
|
||||
let bytes = [0x2A, 0x01, 0x00, 0x00, 0xFF];
|
||||
assert_eq!(
|
||||
inspect(&bytes, 0),
|
||||
"0x2A · 42 · i8 42 · 0b00101010 · u16 298 · u32 298"
|
||||
);
|
||||
assert_eq!(
|
||||
inspect(&bytes, 4),
|
||||
"0xFF · 255 · i8 -1 · 0b11111111",
|
||||
"no word past the end"
|
||||
);
|
||||
assert_eq!((glyph(b'A'), glyph(0), glyph(b' ')), ('A', '·', ' '));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Window};
|
||||
|
||||
mod disasm;
|
||||
mod flame;
|
||||
mod hex;
|
||||
mod network;
|
||||
mod session;
|
||||
mod stack;
|
||||
mod timeline;
|
||||
mod variables;
|
||||
|
||||
pub use disasm::{Disassembly, Instruction};
|
||||
pub use flame::{Flamegraph, Placed, ProfileFrame, place};
|
||||
pub use hex::HexViewer;
|
||||
pub use network::{NetworkInspector, Request};
|
||||
pub use session::{Breakpoint, BreakpointList, DebugCommand, DebugState, DebugToolbar};
|
||||
pub use stack::{CallStack, StackFrame, Thread, ThreadList, ThreadState};
|
||||
pub use timeline::{Span, TimelineProfiler, zoomed};
|
||||
pub use variables::{ValueKind, Variable, VariablesPanel, Watch, WatchPanel};
|
||||
|
||||
/// Tells the owner which row, by index.
|
||||
type OnIndex = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
forms::Checkbox,
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize},
|
||||
typography::{Ellipsis, tabular},
|
||||
};
|
||||
|
||||
/// Whether the program runs or waits.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DebugState {
|
||||
Running,
|
||||
Paused,
|
||||
}
|
||||
|
||||
/// What the debugger is asked to do.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DebugCommand {
|
||||
Continue,
|
||||
Pause,
|
||||
StepOver,
|
||||
StepInto,
|
||||
StepOut,
|
||||
Restart,
|
||||
Stop,
|
||||
}
|
||||
|
||||
type OnCommand = Rc<dyn Fn(DebugCommand, &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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct DebugToolbar {
|
||||
id: ElementId,
|
||||
state: DebugState,
|
||||
on_command: Option<OnCommand>,
|
||||
}
|
||||
|
||||
impl DebugToolbar {
|
||||
pub fn new(id: impl Into<ElementId>, state: DebugState) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
state,
|
||||
on_command: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_command(
|
||||
mut self,
|
||||
handler: impl Fn(DebugCommand, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_command = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DebugToolbar {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
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 rule = || div().w_0().h_4().border_l_1().border_color(colors.border);
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_0p5()
|
||||
.p_1()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.overlay)
|
||||
.shadow(theme.elevation(Elevation::Floating))
|
||||
.child(if paused {
|
||||
button(DebugCommand::Continue, IconName::Play, "Continue F5", true)
|
||||
} else {
|
||||
button(DebugCommand::Pause, IconName::Pause, "Pause F6", true)
|
||||
})
|
||||
.child(button(
|
||||
DebugCommand::StepOver,
|
||||
IconName::RedoDot,
|
||||
"Step over F10",
|
||||
paused,
|
||||
))
|
||||
.child(button(
|
||||
DebugCommand::StepInto,
|
||||
IconName::ArrowDownToDot,
|
||||
"Step into F11",
|
||||
paused,
|
||||
))
|
||||
.child(button(
|
||||
DebugCommand::StepOut,
|
||||
IconName::ArrowUpFromDot,
|
||||
"Step out ⇧F11",
|
||||
paused,
|
||||
))
|
||||
.child(rule())
|
||||
.child(button(
|
||||
DebugCommand::Restart,
|
||||
IconName::RotateCw,
|
||||
"Restart ⇧⌘F5",
|
||||
true,
|
||||
))
|
||||
.child(button(
|
||||
DebugCommand::Stop,
|
||||
IconName::Square,
|
||||
"Stop ⇧F5",
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A breakpoint: where, whether it is on, the condition that guards it, and how often it has hit.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Breakpoint {
|
||||
pub path: SharedString,
|
||||
/// From one.
|
||||
pub line: usize,
|
||||
pub enabled: bool,
|
||||
pub condition: Option<SharedString>,
|
||||
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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct BreakpointList {
|
||||
id: ElementId,
|
||||
breakpoints: Vec<Breakpoint>,
|
||||
on_toggle: Option<OnSwitch>,
|
||||
on_open: Option<OnIndex>,
|
||||
on_remove: Option<OnIndex>,
|
||||
}
|
||||
|
||||
impl BreakpointList {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
breakpoints: impl IntoIterator<Item = Breakpoint>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
breakpoints: breakpoints.into_iter().collect(),
|
||||
on_toggle: None,
|
||||
on_open: None,
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
handler: impl Fn(usize, bool, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_open(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_open = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_remove = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BreakpointList {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let rows: Vec<AnyElement> = self
|
||||
.breakpoints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, point)| {
|
||||
let (folder, name) = point.path.rsplit_once('/').unwrap_or(("", &point.path));
|
||||
let group = SharedString::from(format!("breakpoint-{ix}"));
|
||||
let toggle = self.on_toggle.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 = match toggle {
|
||||
Some(toggle) => {
|
||||
check.on_change(move |on, window, cx| toggle(ix, on, window, cx))
|
||||
}
|
||||
None => check,
|
||||
};
|
||||
div()
|
||||
.id((self.id.clone(), format!("row-{ix}")))
|
||||
.group(group.clone())
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
.when_some(open, |row, open| {
|
||||
row.cursor_pointer()
|
||||
.on_click(move |_, window, cx| open(ix, window, cx))
|
||||
})
|
||||
.child(check)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.size_2()
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(colors.danger)
|
||||
.when(point.enabled, |dot| dot.bg(colors.danger)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(colors.fg)
|
||||
.child(format!("{name}:{}", point.line)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(Ellipsis::new(folder.to_string())),
|
||||
)
|
||||
.children(point.condition.clone().map(|condition| {
|
||||
div()
|
||||
.flex_none()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg_muted)
|
||||
.child(format!("when {condition}"))
|
||||
}))
|
||||
.when(point.hits > 0, |row| {
|
||||
row.child(
|
||||
tabular(div().flex_none().text_color(colors.fg_subtle))
|
||||
.child(format!("×{}", point.hits)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.)
|
||||
.group_hover(group, |actions| actions.opacity(1.))
|
||||
.child(
|
||||
IconButton::new(
|
||||
(self.id.clone(), format!("remove-{ix}")),
|
||||
IconName::X,
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Remove breakpoint")
|
||||
.when_some(
|
||||
remove,
|
||||
|button, remove| {
|
||||
button.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
remove(ix, window, cx)
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::{
|
||||
forms::OnFlag,
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize, Palette, Radius, TextSize},
|
||||
typography::{Ellipsis, format},
|
||||
};
|
||||
|
||||
type OnThread = Rc<dyn Fn(u64, &mut Window, &mut App)>;
|
||||
|
||||
/// A frame of the call stack: its function, where it is, and whether it is library code.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct StackFrame {
|
||||
pub function: SharedString,
|
||||
pub path: SharedString,
|
||||
/// From one.
|
||||
pub line: usize,
|
||||
pub library: bool,
|
||||
}
|
||||
|
||||
/// Frames as shown: each one of the program's, and each run of library frames as one fold, by the frames it holds.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Shown {
|
||||
Frame(usize),
|
||||
Folded(Range<usize>),
|
||||
}
|
||||
|
||||
pub(crate) fn shown(frames: &[StackFrame], libraries: bool) -> Vec<Shown> {
|
||||
let mut out = Vec::new();
|
||||
for (ix, frame) in frames.iter().enumerate() {
|
||||
if !frame.library || libraries {
|
||||
out.push(Shown::Frame(ix));
|
||||
continue;
|
||||
}
|
||||
match out.last_mut() {
|
||||
Some(Shown::Folded(range)) if range.end == ix => range.end = ix + 1,
|
||||
_ => out.push(Shown::Folded(ix..ix + 1)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Where the program is paused, innermost call first: the current frame marked, runs of library frames folded until pressed; a press moves to a frame.
|
||||
#[derive(IntoElement)]
|
||||
pub struct CallStack {
|
||||
id: ElementId,
|
||||
frames: Vec<StackFrame>,
|
||||
current: usize,
|
||||
libraries: bool,
|
||||
on_pick: Option<OnIndex>,
|
||||
on_libraries: Option<OnFlag>,
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
pub fn new(id: impl Into<ElementId>, frames: impl IntoIterator<Item = StackFrame>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
frames: frames.into_iter().collect(),
|
||||
current: 0,
|
||||
libraries: false,
|
||||
on_pick: None,
|
||||
on_libraries: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(mut self, frame: usize) -> Self {
|
||||
assert!(
|
||||
frame < self.frames.len(),
|
||||
"the current frame is on the stack"
|
||||
);
|
||||
self.current = frame;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether library frames show one by one.
|
||||
pub fn libraries(mut self, shown: bool) -> Self {
|
||||
self.libraries = shown;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_pick(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_pick = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_libraries(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_libraries = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CallStack {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let rows: Vec<AnyElement> = shown(&self.frames, self.libraries)
|
||||
.into_iter()
|
||||
.map(|entry| match entry {
|
||||
Shown::Frame(ix) => {
|
||||
let frame = &self.frames[ix];
|
||||
let (current, pick) = (ix == self.current, self.on_pick.clone());
|
||||
let file = frame
|
||||
.path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(&frame.path)
|
||||
.to_string();
|
||||
div()
|
||||
.id((self.id.clone(), format!("frame-{ix}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.cursor_pointer()
|
||||
.when(current, |row| row.bg(colors.warning.opacity(0.12)))
|
||||
.when(!current, |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_none().w_4().children(current.then(|| {
|
||||
Icon::new(IconName::ArrowRight)
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.warning)
|
||||
})))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_color(if frame.library {
|
||||
colors.fg_muted
|
||||
} else {
|
||||
colors.fg
|
||||
})
|
||||
.child(Ellipsis::new(frame.function.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(format!("{file}:{}", frame.line)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
Shown::Folded(range) => {
|
||||
let show = self.on_libraries.clone();
|
||||
div()
|
||||
.id((self.id.clone(), format!("fold-{}", range.start)))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.pl_8()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.cursor_pointer()
|
||||
.text_color(colors.fg_subtle)
|
||||
.hover(|row| row.text_color(colors.fg))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when_some(show, |row, show| {
|
||||
row.on_click(move |_, window, cx| show(true, window, cx))
|
||||
})
|
||||
.child(format!(
|
||||
"⋯ {}",
|
||||
format::plural(range.len() as u64, "library frame", "library frames")
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// How a thread stands.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ThreadState {
|
||||
Running,
|
||||
Paused,
|
||||
/// Stopped on a breakpoint.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl ThreadState {
|
||||
fn look(self, colors: &Palette) -> (IconName, Hsla, &'static str) {
|
||||
match self {
|
||||
Self::Running => (IconName::Play, colors.success, "running"),
|
||||
Self::Paused => (IconName::Pause, colors.fg_muted, "paused"),
|
||||
Self::Stopped => (IconName::CircleDot, colors.danger, "on a breakpoint"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread: its id, its name, and how it stands.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Thread {
|
||||
pub id: u64,
|
||||
pub name: SharedString,
|
||||
pub state: ThreadState,
|
||||
}
|
||||
|
||||
/// The program's threads, each with how it stands; a press picks the one whose stack shows.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ThreadList {
|
||||
id: ElementId,
|
||||
threads: Vec<Thread>,
|
||||
selected: Option<u64>,
|
||||
on_pick: Option<OnThread>,
|
||||
}
|
||||
|
||||
impl ThreadList {
|
||||
pub fn new(id: impl Into<ElementId>, threads: impl IntoIterator<Item = Thread>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
threads: threads.into_iter().collect(),
|
||||
selected: None,
|
||||
on_pick: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, thread: u64) -> Self {
|
||||
self.selected = Some(thread);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_pick(mut self, handler: impl Fn(u64, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_pick = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ThreadList {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(self.threads.iter().map(|thread| {
|
||||
let (icon, tint, words) = thread.state.look(&colors);
|
||||
let selected = self.selected == Some(thread.id);
|
||||
let (pick, id) = (self.on_pick.clone(), thread.id);
|
||||
div()
|
||||
.id((self.id.clone(), format!("thread-{}", thread.id)))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.cursor_pointer()
|
||||
.when(selected, |row| row.bg(colors.active))
|
||||
.when(!selected, |row| row.hover(|row| row.bg(colors.hover)))
|
||||
.when_some(pick, |row, pick| {
|
||||
row.on_click(move |_, window, cx| pick(id, window, cx))
|
||||
})
|
||||
.child(Icon::new(icon).size(IconSize::Sm).color(tint))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_color(colors.fg)
|
||||
.child(thread.name.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(format!("#{} · {words}", thread.id)),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn frame(library: bool) -> StackFrame {
|
||||
StackFrame {
|
||||
function: "f".into(),
|
||||
path: "a.rs".into(),
|
||||
line: 1,
|
||||
library,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_of_library_frames_fold_into_one() {
|
||||
let frames = [
|
||||
frame(false),
|
||||
frame(true),
|
||||
frame(true),
|
||||
frame(false),
|
||||
frame(true),
|
||||
];
|
||||
assert_eq!(
|
||||
shown(&frames, false),
|
||||
[
|
||||
Shown::Frame(0),
|
||||
Shown::Folded(1..3),
|
||||
Shown::Frame(3),
|
||||
Shown::Folded(4..5)
|
||||
]
|
||||
);
|
||||
assert_eq!(shown(&frames, true).len(), 5, "shown one by one when asked");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, Entity, FontWeight, Hsla, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
forms::{Input, TextInput},
|
||||
primitives::{Disclosure, IconName},
|
||||
theme::{ActiveTheme, ControlSize, Palette, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
|
||||
/// What a value is, which picks its color.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ValueKind {
|
||||
Text,
|
||||
Number,
|
||||
Bool,
|
||||
Null,
|
||||
Object,
|
||||
List,
|
||||
Function,
|
||||
}
|
||||
|
||||
impl ValueKind {
|
||||
fn color(self, colors: &Palette) -> Hsla {
|
||||
let syntax = &colors.syntax;
|
||||
match self {
|
||||
Self::Text => syntax.string,
|
||||
Self::Number => syntax.number,
|
||||
Self::Bool | Self::Null => syntax.keyword,
|
||||
Self::Object | Self::List => syntax.type_name,
|
||||
Self::Function => syntax.function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A variable: its name, its value as the debugger prints it, its kind and type, and its fields or items.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Variable {
|
||||
pub name: SharedString,
|
||||
pub value: SharedString,
|
||||
pub kind: ValueKind,
|
||||
pub type_name: Option<SharedString>,
|
||||
pub children: Vec<Variable>,
|
||||
}
|
||||
|
||||
/// Rows for `variables` under `path`, their children when open: each row's key, depth and variable.
|
||||
pub(crate) fn flat<'a>(
|
||||
variables: &'a [Variable],
|
||||
path: &str,
|
||||
depth: usize,
|
||||
open: &[SharedString],
|
||||
out: &mut Vec<(SharedString, usize, &'a Variable)>,
|
||||
) {
|
||||
for variable in variables {
|
||||
let key: SharedString = format!("{path}/{}", variable.name).into();
|
||||
let opened = open.contains(&key);
|
||||
out.push((key.clone(), depth, variable));
|
||||
if opened {
|
||||
flat(&variable.children, &key, depth + 1, open, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OnToggle = Rc<dyn Fn(&SharedString, bool, &mut Window, &mut App)>;
|
||||
|
||||
/// Variables by scope, as a tree: each with its value colored by kind and its type, fields and items opening under it; values that changed at the last step washed.
|
||||
#[derive(IntoElement)]
|
||||
pub struct VariablesPanel {
|
||||
id: ElementId,
|
||||
scopes: Vec<(SharedString, Vec<Variable>)>,
|
||||
open: Vec<SharedString>,
|
||||
changed: Vec<SharedString>,
|
||||
on_toggle: Option<OnToggle>,
|
||||
}
|
||||
|
||||
impl VariablesPanel {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
scopes: impl IntoIterator<Item = (SharedString, Vec<Variable>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
scopes: scopes.into_iter().collect(),
|
||||
open: Vec::new(),
|
||||
changed: Vec::new(),
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scopes and variables shown open, by key: `scope`, `scope/name`, `scope/name/field`.
|
||||
pub fn open(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
|
||||
self.open = keys.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Variables whose values changed at the last step, by key.
|
||||
pub fn changed(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
|
||||
self.changed = keys.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for VariablesPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let toggle_row = |key: SharedString, opens: bool, opened: bool, depth: usize| {
|
||||
let toggle = self.on_toggle.clone();
|
||||
div()
|
||||
.id((self.id.clone(), key.clone()))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.pl(theme.text_size(TextSize::Sm) * (depth as f32 * 1.1 + 0.4))
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when(opens, |row| {
|
||||
row.cursor_pointer().when_some(toggle, |row, toggle| {
|
||||
row.on_click(move |_, window, cx| toggle(&key, !opened, window, cx))
|
||||
})
|
||||
})
|
||||
.child(div().flex_none().w_4().children(opens.then(|| {
|
||||
Disclosure::new(
|
||||
(self.id.clone(), format!("chevron-{depth}-{opened}")),
|
||||
opened,
|
||||
)
|
||||
})))
|
||||
};
|
||||
let mut rows: Vec<AnyElement> = Vec::new();
|
||||
for (scope, variables) in &self.scopes {
|
||||
let scope_key = scope.clone();
|
||||
let opened = self.open.contains(&scope_key);
|
||||
rows.push(
|
||||
toggle_row(scope_key.clone(), true, opened, 0)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(colors.fg_muted)
|
||||
.child(scope.clone())
|
||||
.into_any_element(),
|
||||
);
|
||||
if !opened {
|
||||
continue;
|
||||
}
|
||||
let mut shown = Vec::new();
|
||||
flat(variables, scope, 1, &self.open, &mut shown);
|
||||
for (key, depth, variable) in shown {
|
||||
let opens = !variable.children.is_empty();
|
||||
let changed = self.changed.contains(&key);
|
||||
rows.push(
|
||||
toggle_row(key.clone(), opens, self.open.contains(&key), depth)
|
||||
.font_family(theme.mono_family.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.syntax.property)
|
||||
.child(variable.name.clone()),
|
||||
)
|
||||
.child(div().flex_none().text_color(colors.fg_subtle).child(":"))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.px_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.when(changed, |value| value.bg(colors.warning.opacity(0.18)))
|
||||
.text_color(variable.kind.color(&colors))
|
||||
.child(Ellipsis::new(variable.value.clone())),
|
||||
)
|
||||
.children(
|
||||
variable.type_name.clone().map(|kind| {
|
||||
div().flex_none().text_color(colors.fg_subtle).child(kind)
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
}
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// A watched expression and what it gives: a value and its kind, or the error it raised.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Watch {
|
||||
pub expression: SharedString,
|
||||
pub value: Result<(SharedString, ValueKind), SharedString>,
|
||||
}
|
||||
|
||||
/// Expressions watched at every step, each with its value or its error; a field adds one, and each removes.
|
||||
#[derive(IntoElement)]
|
||||
pub struct WatchPanel {
|
||||
id: ElementId,
|
||||
watches: Vec<Watch>,
|
||||
field: Entity<TextInput>,
|
||||
on_remove: Option<OnIndex>,
|
||||
}
|
||||
|
||||
impl WatchPanel {
|
||||
/// `field` takes a new expression; its owner adds it on Enter.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
watches: impl IntoIterator<Item = Watch>,
|
||||
field: &Entity<TextInput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
watches: watches.into_iter().collect(),
|
||||
field: field.clone(),
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_remove = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WatchPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(self.watches.iter().enumerate().map(|(ix, watch)| {
|
||||
let group = SharedString::from(format!("watch-{ix}"));
|
||||
let remove = self.on_remove.clone();
|
||||
let (value, color) = match &watch.value {
|
||||
Ok((value, kind)) => (value.clone(), kind.color(&colors)),
|
||||
Err(error) => (error.clone(), colors.danger),
|
||||
};
|
||||
div()
|
||||
.id((self.id.clone(), format!("row-{ix}")))
|
||||
.group(group.clone())
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
.font_family(theme.mono_family.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.fg)
|
||||
.child(watch.expression.clone()),
|
||||
)
|
||||
.child(div().flex_none().text_color(colors.fg_subtle).child("="))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_color(color)
|
||||
.child(Ellipsis::new(value)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.)
|
||||
.group_hover(group, |actions| actions.opacity(1.))
|
||||
.child(
|
||||
IconButton::new(
|
||||
(self.id.clone(), format!("remove-{ix}")),
|
||||
IconName::X,
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Stop watching")
|
||||
.when_some(
|
||||
remove,
|
||||
|button, remove| {
|
||||
button.on_click(move |_, window, cx| remove(ix, window, cx))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}))
|
||||
.child(Input::new(&self.field).size(ControlSize::Sm))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn variable(name: &str, children: Vec<Variable>) -> Variable {
|
||||
Variable {
|
||||
name: name.to_string().into(),
|
||||
value: "".into(),
|
||||
kind: ValueKind::Object,
|
||||
type_name: None,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_variables_show_their_fields_under_them() {
|
||||
let all = [
|
||||
variable(
|
||||
"palette",
|
||||
vec![variable("colors", vec![variable("accent", vec![])])],
|
||||
),
|
||||
variable("count", vec![]),
|
||||
];
|
||||
let mut rows = Vec::new();
|
||||
flat(&all, "Locals", 1, &["Locals/palette".into()], &mut rows);
|
||||
let keys: Vec<(&str, usize)> = rows
|
||||
.iter()
|
||||
.map(|(key, depth, _)| (key.as_ref(), *depth))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
keys,
|
||||
[
|
||||
("Locals/palette", 1),
|
||||
("Locals/palette/colors", 2),
|
||||
("Locals/count", 1)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user