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:
22 files changed
+3240
-14
No files matched your search
@@ -109,6 +109,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Charts lay out in `f32` pixels of their own box, tested without gpui, and paint through `charts::paint::at`: a canvas's paint gets window coordinates, and `with_element_offset` works only in prepaint. Their sizes come from `theme.chart()`.
|
||||
- A label pinned to a point sits in a zero-size absolute box that centers it with flex (`charts::axes`), so no offset guesses at text size.
|
||||
- Tokens live in `src/theme`; the raw `px()` scan skips that folder only.
|
||||
- A callback field holds a named `type` alias; clippy's type_complexity rejects `Option<Rc<dyn Fn(..)>>`. A chapter shares one alias per shape.
|
||||
- A terminal takes Tab and Shift-Tab through bindings under `ElyTerminal`, ahead of focus moves; other keys go through `terminal::keys` in a key listener, and typed text through its input handler.
|
||||
- A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host.
|
||||
- gpui 0.2.2 has no `(ElementId, usize)` id. A child id names its index: `(self.id.clone(), format!("row-{ix}"))`.
|
||||
|
||||
@@ -67,7 +67,7 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
- [x] T17e Editor · Status items
|
||||
- [x] T18 Terminal
|
||||
- [x] T19 Git
|
||||
- [ ] T20 Debug
|
||||
- [x] T20 Debug
|
||||
- [ ] T21a Documents · Editing — `tasks/ch21-30.md`
|
||||
- [ ] T21b Documents · Reading
|
||||
- [ ] T21c Documents · Knowledge
|
||||
@@ -245,3 +245,4 @@ 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 |
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-arrow-down-to-dot"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 2v14" />
|
||||
<path d="m19 9-7 7-7-7" />
|
||||
<circle cx="12" cy="21" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,17 @@
|
||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-arrow-up-from-dot"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m5 9 7-7 7 7" />
|
||||
<path d="M12 16V2" />
|
||||
<circle cx="12" cy="21" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 383 B |
@@ -0,0 +1,17 @@
|
||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-redo-dot"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="17" r="1" />
|
||||
<path d="M21 7v6h-6" />
|
||||
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 405 B |
@@ -0,0 +1,163 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_gpui_component::{
|
||||
debug::{Disassembly, HexViewer, Instruction},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
|
||||
|
||||
use crate::ui::{keep, section, set};
|
||||
|
||||
/// A few hundred bytes of a process's memory, the same each run.
|
||||
fn memory() -> Rc<Vec<u8>> {
|
||||
let mut bytes: Vec<u8> = b"\x7fELF\x02\x01\x01\x00".to_vec();
|
||||
bytes.extend(b"palette\0accent\0fallback\0");
|
||||
bytes.extend((0u32..256).map(|ix| (ix.wrapping_mul(37) ^ (ix >> 3)) as u8));
|
||||
bytes.extend(b"Blend toward white by lift\0");
|
||||
Rc::new(bytes)
|
||||
}
|
||||
|
||||
fn listing() -> Rc<Vec<Instruction>> {
|
||||
let at = |address: u64,
|
||||
bytes: &[u8],
|
||||
mnemonic: &'static str,
|
||||
operands: &'static str,
|
||||
comment: Option<&'static str>,
|
||||
source: Option<&'static str>| Instruction {
|
||||
address,
|
||||
bytes: bytes.to_vec(),
|
||||
mnemonic: mnemonic.into(),
|
||||
operands: operands.into(),
|
||||
comment: comment.map(Into::into),
|
||||
source: source.map(Into::into),
|
||||
};
|
||||
Rc::new(vec![
|
||||
at(
|
||||
0x1000_4a20,
|
||||
&[0x55],
|
||||
"push",
|
||||
"rbp",
|
||||
None,
|
||||
Some("pub fn lookup(&self, name: &str, lift: f32) -> Color {"),
|
||||
),
|
||||
at(
|
||||
0x1000_4a21,
|
||||
&[0x48, 0x89, 0xe5],
|
||||
"mov",
|
||||
"rbp, rsp",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
at(
|
||||
0x1000_4a24,
|
||||
&[0x48, 0x83, 0xec, 0x30],
|
||||
"sub",
|
||||
"rsp, 0x30",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
at(
|
||||
0x1000_4a28,
|
||||
&[0x48, 0x89, 0x7d, 0xe8],
|
||||
"mov",
|
||||
"qword ptr [rbp - 0x18], rdi",
|
||||
Some("self"),
|
||||
None,
|
||||
),
|
||||
at(
|
||||
0x1000_4a2c,
|
||||
&[0xf3, 0x0f, 0x11, 0x45, 0xe4],
|
||||
"movss",
|
||||
"dword ptr [rbp - 0x1c], xmm0",
|
||||
Some("lift"),
|
||||
None,
|
||||
),
|
||||
at(
|
||||
0x1000_4a31,
|
||||
&[0xe8, 0x9a, 0x01, 0x00, 0x00],
|
||||
"call",
|
||||
"0x100004bd0",
|
||||
Some("BTreeMap::get"),
|
||||
Some(" let base = self.colors.get(name).copied().unwrap_or(self.fallback);"),
|
||||
),
|
||||
at(
|
||||
0x1000_4a36,
|
||||
&[0x48, 0x85, 0xc0],
|
||||
"test",
|
||||
"rax, rax",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
at(0x1000_4a39, &[0x74, 0x12], "je", "0x100004a4d", None, None),
|
||||
at(
|
||||
0x1000_4a3b,
|
||||
&[0xf3, 0x0f, 0x10, 0x45, 0xe4],
|
||||
"movss",
|
||||
"xmm0, dword ptr [rbp - 0x1c]",
|
||||
None,
|
||||
Some(" base.lift(lift)"),
|
||||
),
|
||||
at(
|
||||
0x1000_4a40,
|
||||
&[0xe8, 0x4b, 0x02, 0x00, 0x00],
|
||||
"call",
|
||||
"0x100004c90",
|
||||
Some("Color::lift"),
|
||||
None,
|
||||
),
|
||||
at(
|
||||
0x1000_4a45,
|
||||
&[0x48, 0x83, 0xc4, 0x30],
|
||||
"add",
|
||||
"rsp, 0x30",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
at(0x1000_4a49, &[0x5d], "pop", "rbp", None, None),
|
||||
at(0x1000_4a4a, &[0xc3], "ret", "", None, Some("}")),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn machine(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let picked = keep("debug-byte", || 8usize, window, cx);
|
||||
let marks = keep("debug-marks", || vec![5usize], window, cx);
|
||||
let (now_picked, now_marks) = (*picked.read(cx), marks.read(cx).clone());
|
||||
let (pick, toggle) = (picked.clone(), marks.clone());
|
||||
let theme = cx.theme();
|
||||
let frame = || {
|
||||
div()
|
||||
.w(px(840.))
|
||||
.p_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
};
|
||||
section(
|
||||
"MemoryViewer / HexViewer / Disassembly",
|
||||
"Memory as a hex dump, sixteen bytes a row in fours beside the same bytes as text; the byte under the pointer lights on both sides and the picked one reads below as numbers. Machine code with its source lines, registers and numbers colored, the current instruction marked; press the gutter to set a breakpoint.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
frame().h(px(220.)).child(
|
||||
HexViewer::new("debug-hex", memory())
|
||||
.base(0x7ffd_5a20_1000)
|
||||
.selected(now_picked..now_picked + 4)
|
||||
.on_select(move |at, _, cx| set(&pick, at, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
frame().h(px(300.)).child(
|
||||
Disassembly::new("debug-disasm", listing())
|
||||
.current(8)
|
||||
.breakpoints(now_marks)
|
||||
.on_breakpoint(move |ix, _, cx| {
|
||||
let mut next = toggle.read(cx).clone();
|
||||
match next.iter().position(|mark| *mark == ix) {
|
||||
Some(at) => drop(next.remove(at)),
|
||||
None => next.push(ix),
|
||||
}
|
||||
set(&toggle, next, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
mod machine;
|
||||
mod profiling;
|
||||
mod session;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 20,
|
||||
slug: "debug",
|
||||
title: "Debug",
|
||||
summary: "A paused program: its breakpoints, stack, threads and values, memory and machine code; profiles and network.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[Step::Rest];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(session::session(window, cx))
|
||||
.child(session::breakpoints_and_stack(window, cx))
|
||||
.child(session::values(window, cx))
|
||||
.child(machine::machine(window, cx))
|
||||
.child(profiling::profiles(window, cx))
|
||||
.child(profiling::network(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_gpui_component::{
|
||||
debug::{Flamegraph, NetworkInspector, ProfileFrame, Request, Span, TimelineProfiler},
|
||||
forms::{Input, TextInput},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::ui::{keep, section, set};
|
||||
|
||||
fn frame(name: &str, own: u64, calls: Vec<ProfileFrame>) -> ProfileFrame {
|
||||
ProfileFrame {
|
||||
name: name.to_string().into(),
|
||||
own,
|
||||
calls,
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame's worth of an app's time, sampled.
|
||||
fn profile() -> ProfileFrame {
|
||||
frame(
|
||||
"main",
|
||||
4,
|
||||
vec![
|
||||
frame(
|
||||
"run_loop",
|
||||
6,
|
||||
vec![
|
||||
frame(
|
||||
"draw",
|
||||
10,
|
||||
vec![
|
||||
frame(
|
||||
"layout",
|
||||
38,
|
||||
vec![
|
||||
frame("measure_text", 22, vec![]),
|
||||
frame("taffy::compute", 30, vec![]),
|
||||
],
|
||||
),
|
||||
frame(
|
||||
"paint",
|
||||
26,
|
||||
vec![
|
||||
frame("shape_line", 18, vec![]),
|
||||
frame("rasterize", 12, vec![]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
frame("dispatch_event", 14, vec![frame("on_click", 9, vec![])]),
|
||||
],
|
||||
),
|
||||
frame("io_thread", 20, vec![frame("read", 16, vec![])]),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn spans() -> Rc<Vec<Span>> {
|
||||
let span = |track, name: &str, start, end| Span {
|
||||
track,
|
||||
name: name.to_string().into(),
|
||||
start,
|
||||
end,
|
||||
};
|
||||
Rc::new(vec![
|
||||
span(0, "event", 0.0, 1.2),
|
||||
span(0, "layout", 1.2, 5.6),
|
||||
span(0, "paint", 5.6, 9.1),
|
||||
span(0, "event", 16.6, 17.3),
|
||||
span(0, "layout", 17.3, 20.4),
|
||||
span(0, "paint", 20.4, 23.0),
|
||||
span(1, "shape", 2.0, 4.8),
|
||||
span(1, "shape", 18.0, 19.6),
|
||||
span(2, "gc", 9.5, 12.0),
|
||||
span(3, "read", 0.5, 14.0),
|
||||
span(3, "parse", 14.0, 15.2),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn profiles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let focus = keep("debug-focus", Vec::<usize>::new, window, cx);
|
||||
let hovered = keep("debug-hovered", || None::<Vec<usize>>, window, cx);
|
||||
let range = keep("debug-range", || (0.0_f64, 25.0_f64), window, cx);
|
||||
let picked = keep("debug-span", || Some(1usize), window, cx);
|
||||
let (now_focus, now_hovered, now_range, now_picked) = (
|
||||
focus.read(cx).clone(),
|
||||
hovered.read(cx).clone(),
|
||||
*range.read(cx),
|
||||
*picked.read(cx),
|
||||
);
|
||||
let (zoom, hover, pan, pick) = (
|
||||
focus.clone(),
|
||||
hovered.clone(),
|
||||
range.clone(),
|
||||
picked.clone(),
|
||||
);
|
||||
let timeline = TimelineProfiler::new(
|
||||
"debug-timeline",
|
||||
["Main", "Text", "Memory", "IO"],
|
||||
spans(),
|
||||
now_range,
|
||||
)
|
||||
.on_range(move |next, _, cx| set(&pan, next, cx))
|
||||
.on_select(move |ix, _, cx| set(&pick, Some(ix), cx));
|
||||
let timeline = match now_picked {
|
||||
Some(ix) => timeline.selected(ix),
|
||||
None => timeline,
|
||||
};
|
||||
section(
|
||||
"PerformanceProfiler / Flamegraph / TimelineProfiler",
|
||||
"Where the time went: each function as wide as its samples with its callees under it; press a frame to zoom in and a caller to zoom back out. Work over time on tracks; the wheel zooms around the pointer and a sideways wheel pans.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(840.)).child(
|
||||
Flamegraph::new("debug-flame", profile())
|
||||
.focus(now_focus)
|
||||
.hovered(now_hovered)
|
||||
.on_focus(move |path, _, cx| set(&zoom, path, cx))
|
||||
.on_hover(move |path, _, cx| {
|
||||
if *hover.read(cx) != path {
|
||||
set(&hover, path, cx)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(840.)).child(timeline))
|
||||
}
|
||||
|
||||
fn requests() -> Vec<Request> {
|
||||
let request = |method: &'static str,
|
||||
url: &'static str,
|
||||
status,
|
||||
kind: &'static str,
|
||||
size,
|
||||
start,
|
||||
duration,
|
||||
body: Option<&'static str>| Request {
|
||||
method: method.into(),
|
||||
url: url.into(),
|
||||
status,
|
||||
kind: kind.into(),
|
||||
size,
|
||||
start,
|
||||
duration,
|
||||
headers: vec![
|
||||
(
|
||||
"content-type".into(),
|
||||
if kind == "fetch" {
|
||||
"application/json".into()
|
||||
} else {
|
||||
"text/html".into()
|
||||
},
|
||||
),
|
||||
("cache-control".into(), "no-store".into()),
|
||||
],
|
||||
body: body.map(Into::into),
|
||||
};
|
||||
vec![
|
||||
request(
|
||||
"GET",
|
||||
"https://ely.dev/",
|
||||
Some(200),
|
||||
"document",
|
||||
18_400,
|
||||
0.0,
|
||||
120.0,
|
||||
None,
|
||||
),
|
||||
request(
|
||||
"GET",
|
||||
"https://ely.dev/static/app.js",
|
||||
Some(200),
|
||||
"script",
|
||||
212_000,
|
||||
130.0,
|
||||
240.0,
|
||||
None,
|
||||
),
|
||||
request(
|
||||
"GET",
|
||||
"https://ely.dev/static/inter.woff2",
|
||||
Some(304),
|
||||
"font",
|
||||
0,
|
||||
140.0,
|
||||
60.0,
|
||||
None,
|
||||
),
|
||||
request(
|
||||
"GET",
|
||||
"https://api.ely.dev/v1/quotes?symbol=ELY",
|
||||
Some(200),
|
||||
"fetch",
|
||||
2_300,
|
||||
380.0,
|
||||
95.0,
|
||||
Some("{ \"symbol\": \"ELY\", \"price\": 128.4, \"change\": 0.012 }"),
|
||||
),
|
||||
request(
|
||||
"POST",
|
||||
"https://api.ely.dev/v1/orders",
|
||||
Some(503),
|
||||
"fetch",
|
||||
180,
|
||||
420.0,
|
||||
310.0,
|
||||
Some("{ \"error\": \"orders are paused\" }"),
|
||||
),
|
||||
request(
|
||||
"GET",
|
||||
"https://api.ely.dev/v1/stream",
|
||||
None,
|
||||
"fetch",
|
||||
0,
|
||||
500.0,
|
||||
400.0,
|
||||
None,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn network(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let picked = keep("debug-request", || Some(4usize), window, cx);
|
||||
let query = window.use_keyed_state("debug-network-query", cx, |window, cx| {
|
||||
TextInput::new(window, cx).placeholder("Filter addresses")
|
||||
});
|
||||
let now = *picked.read(cx);
|
||||
let text: SharedString = query.read(cx).text().to_string().into();
|
||||
let pick = picked.clone();
|
||||
let inspector = NetworkInspector::new("debug-network", requests())
|
||||
.query(text)
|
||||
.on_select(move |ix, _, cx| set(&pick, Some(ix), cx));
|
||||
let inspector = match now {
|
||||
Some(ix) => inspector.selected(ix),
|
||||
None => inspector,
|
||||
};
|
||||
section(
|
||||
"NetworkInspector",
|
||||
"Requests in the order they started, with status, kind, size and time, and a waterfall of when each ran; a filter keeps matching addresses. Press one to read its headers and body.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(320.)).child(Input::new(&query)))
|
||||
.child(div().w(px(840.)).child(inspector))
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
use ely_gpui_component::{
|
||||
debug::{
|
||||
Breakpoint, BreakpointList, CallStack, DebugCommand, DebugState, DebugToolbar, StackFrame,
|
||||
Thread, ThreadList, ThreadState, ValueKind, Variable, VariablesPanel, Watch, WatchPanel,
|
||||
},
|
||||
forms::{InputEvent, TextInput},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::ui::{keep, row, section, set};
|
||||
|
||||
fn frames() -> Vec<StackFrame> {
|
||||
let frame = |function: &str, path: &str, line, library| StackFrame {
|
||||
function: function.to_string().into(),
|
||||
path: path.to_string().into(),
|
||||
line,
|
||||
library,
|
||||
};
|
||||
vec![
|
||||
frame("palette::Palette::lookup", "src/palette.rs", 48, false),
|
||||
frame("palette::tints::{{closure}}", "src/main.rs", 65, false),
|
||||
frame(
|
||||
"core::iter::adapters::map::map_fold",
|
||||
"core/src/iter/adapters/map.rs",
|
||||
88,
|
||||
true,
|
||||
),
|
||||
frame(
|
||||
"core::iter::traits::iterator::Iterator::fold",
|
||||
"core/src/iter/traits/iterator.rs",
|
||||
2587,
|
||||
true,
|
||||
),
|
||||
frame(
|
||||
"alloc::vec::Vec::extend_trusted",
|
||||
"alloc/src/vec/mod.rs",
|
||||
3301,
|
||||
true,
|
||||
),
|
||||
frame("palette::main", "src/main.rs", 65, false),
|
||||
frame("std::rt::lang_start", "std/src/rt.rs", 206, true),
|
||||
]
|
||||
}
|
||||
|
||||
fn scopes() -> Vec<(SharedString, Vec<Variable>)> {
|
||||
let value = |name: &str, value: &str, kind, type_name: Option<&str>, children| Variable {
|
||||
name: name.to_string().into(),
|
||||
value: value.to_string().into(),
|
||||
kind,
|
||||
type_name: type_name.map(|name| name.to_string().into()),
|
||||
children,
|
||||
};
|
||||
vec![
|
||||
(
|
||||
"Locals".into(),
|
||||
vec![
|
||||
value(
|
||||
"self",
|
||||
"&Palette { colors: {…}, fallback: Color }",
|
||||
ValueKind::Object,
|
||||
Some("&Palette"),
|
||||
vec![
|
||||
value(
|
||||
"colors",
|
||||
"{\"accent\": Color, \"fg\": Color}",
|
||||
ValueKind::Object,
|
||||
Some("BTreeMap<String, Color>"),
|
||||
vec![],
|
||||
),
|
||||
value(
|
||||
"fallback",
|
||||
"Color { red: 0.1, green: 0.1, blue: 0.1 }",
|
||||
ValueKind::Object,
|
||||
Some("Color"),
|
||||
vec![
|
||||
value("red", "0.1", ValueKind::Number, Some("f32"), vec![]),
|
||||
value("green", "0.1", ValueKind::Number, Some("f32"), vec![]),
|
||||
value("blue", "0.1", ValueKind::Number, Some("f32"), vec![]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
value("name", "\"accent\"", ValueKind::Text, Some("&str"), vec![]),
|
||||
value("lift", "0.75", ValueKind::Number, Some("f32"), vec![]),
|
||||
value("clamped", "false", ValueKind::Bool, Some("bool"), vec![]),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Statics".into(),
|
||||
vec![value(
|
||||
"WHITE",
|
||||
"Color { red: 1.0, green: 1.0, blue: 1.0 }",
|
||||
ValueKind::Object,
|
||||
Some("Color"),
|
||||
vec![],
|
||||
)],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn session(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("debug-state", || DebugState::Paused, window, cx);
|
||||
let said = keep("debug-said", || None::<SharedString>, window, cx);
|
||||
let (now_state, told) = (*state.read(cx), said.read(cx).clone());
|
||||
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.",
|
||||
cx,
|
||||
)
|
||||
.child(row().child(DebugToolbar::new("debug-toolbar", now_state).on_command(move |which, _, cx| {
|
||||
let next = match which {
|
||||
DebugCommand::Pause => DebugState::Paused,
|
||||
DebugCommand::Continue | DebugCommand::Restart => DebugState::Running,
|
||||
_ => *command.read(cx),
|
||||
};
|
||||
set(&command, next, cx);
|
||||
set(&tell, Some(format!("{which:?}").into()), cx);
|
||||
})))
|
||||
.children(told.map(Caption::new))
|
||||
}
|
||||
|
||||
pub fn breakpoints_and_stack(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let points = keep(
|
||||
"debug-breakpoints",
|
||||
|| {
|
||||
let point = |path: &str, line, enabled, condition: Option<&str>, hits| Breakpoint {
|
||||
path: path.to_string().into(),
|
||||
line,
|
||||
enabled,
|
||||
condition: condition.map(|text| text.to_string().into()),
|
||||
hits,
|
||||
};
|
||||
vec![
|
||||
point("src/palette.rs", 48, true, None, 3),
|
||||
point("src/main.rs", 65, true, Some("step > 2"), 1),
|
||||
point("src/theme/tokens.rs", 112, false, None, 0),
|
||||
]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let current = keep("debug-frame", || 0usize, window, cx);
|
||||
let libraries = keep("debug-libraries", || false, window, cx);
|
||||
let thread = keep("debug-thread", || 1u64, window, cx);
|
||||
let (now_points, now_current, now_libraries, now_thread) = (
|
||||
points.read(cx).clone(),
|
||||
*current.read(cx),
|
||||
*libraries.read(cx),
|
||||
*thread.read(cx),
|
||||
);
|
||||
let (toggle, remove, pick, show, choose) = (
|
||||
points.clone(),
|
||||
points.clone(),
|
||||
current.clone(),
|
||||
libraries.clone(),
|
||||
thread.clone(),
|
||||
);
|
||||
let threads = [
|
||||
Thread {
|
||||
id: 1,
|
||||
name: "main".into(),
|
||||
state: ThreadState::Stopped,
|
||||
},
|
||||
Thread {
|
||||
id: 2,
|
||||
name: "renderer".into(),
|
||||
state: ThreadState::Paused,
|
||||
},
|
||||
Thread {
|
||||
id: 3,
|
||||
name: "io-worker".into(),
|
||||
state: ThreadState::Paused,
|
||||
},
|
||||
];
|
||||
section(
|
||||
"BreakpointList / BreakpointGutter / CallStack / ThreadList",
|
||||
"Every breakpoint with its condition and hits; the stack where the program waits, library frames folded until pressed; and the threads, one picked. The gutter's breakpoints are the editor's (CodeEditor).",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div().w(px(400.)).child(
|
||||
BreakpointList::new("debug-breakpoint-list", now_points)
|
||||
.on_toggle(move |ix, on, _, cx| {
|
||||
let mut next = toggle.read(cx).clone();
|
||||
next[ix].enabled = on;
|
||||
set(&toggle, next, cx)
|
||||
})
|
||||
.on_remove(move |ix, _, cx| {
|
||||
let mut next = remove.read(cx).clone();
|
||||
next.remove(ix);
|
||||
set(&remove, next, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w(px(400.)).flex().flex_col().gap_4().child(
|
||||
CallStack::new("debug-stack", frames())
|
||||
.current(now_current)
|
||||
.libraries(now_libraries)
|
||||
.on_pick(move |ix, _, cx| set(&pick, ix, cx))
|
||||
.on_libraries(move |on, _, cx| set(&show, on, cx)),
|
||||
)
|
||||
.child(ThreadList::new("debug-threads", threads).selected(now_thread).on_pick(move |id, _, cx| set(&choose, id, cx))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn values(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = keep(
|
||||
"debug-open",
|
||||
|| vec![SharedString::from("Locals"), "Locals/self".into()],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let watches = keep(
|
||||
"debug-watches",
|
||||
|| {
|
||||
vec![
|
||||
Watch {
|
||||
expression: "lift * 2.0".into(),
|
||||
value: Ok(("1.5".into(), ValueKind::Number)),
|
||||
},
|
||||
Watch {
|
||||
expression: "self.colors.len()".into(),
|
||||
value: Ok(("2".into(), ValueKind::Number)),
|
||||
},
|
||||
Watch {
|
||||
expression: "missing".into(),
|
||||
value: Err("cannot find value `missing` in this scope".into()),
|
||||
},
|
||||
]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let field = window.use_keyed_state("debug-watch-field", cx, |window, cx| {
|
||||
TextInput::new(window, cx).placeholder("Add an expression")
|
||||
});
|
||||
window.use_keyed_state("debug-watch-sub", cx, {
|
||||
let (field, watches) = (field.clone(), watches.clone());
|
||||
move |_, cx| {
|
||||
cx.subscribe(&field, move |_, input, event: &InputEvent, cx| {
|
||||
if *event != InputEvent::Submit {
|
||||
return;
|
||||
}
|
||||
let text = input.read(cx).text().trim().to_string();
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut next = watches.read(cx).clone();
|
||||
next.push(Watch {
|
||||
expression: text.into(),
|
||||
value: Ok(("0.75".into(), ValueKind::Number)),
|
||||
});
|
||||
set(&watches, next, cx);
|
||||
input.update(cx, |input, cx| input.set_text("", cx));
|
||||
})
|
||||
}
|
||||
});
|
||||
let (now_open, now_watches) = (open.read(cx).clone(), watches.read(cx).clone());
|
||||
let (toggle, remove) = (open.clone(), watches.clone());
|
||||
section(
|
||||
"VariablesPanel / WatchPanel / ConsoleREPL",
|
||||
"Variables by scope as a tree, colored by kind, with the value that changed at the last step washed; expressions watched at every step, errors in red. The REPL beside a paused program is the editor's DebugConsole.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div().w(px(460.)).child(
|
||||
VariablesPanel::new("debug-variables", scopes())
|
||||
.open(now_open)
|
||||
.changed([SharedString::from("Locals/lift")])
|
||||
.on_toggle(move |key, on, _, cx| {
|
||||
let mut next = toggle.read(cx).clone();
|
||||
next.retain(|kept| kept != key);
|
||||
if on {
|
||||
next.push(key.clone());
|
||||
}
|
||||
set(&toggle, next, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w(px(340.)).child(
|
||||
WatchPanel::new("debug-watch", now_watches, &field).on_remove(move |ix, _, cx| {
|
||||
let mut next = remove.read(cx).clone();
|
||||
next.remove(ix);
|
||||
set(&remove, next, cx)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod buttons;
|
||||
mod charts;
|
||||
mod data;
|
||||
mod debug;
|
||||
mod editor;
|
||||
mod feedback;
|
||||
mod finance;
|
||||
@@ -53,6 +54,7 @@ pub const ALL: &[Page] = &[
|
||||
editor::PAGE,
|
||||
terminal::PAGE,
|
||||
git::PAGE,
|
||||
debug::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod assets;
|
||||
pub mod buttons;
|
||||
pub mod charts;
|
||||
pub mod data_display;
|
||||
pub mod debug;
|
||||
pub mod editor;
|
||||
pub mod feedback;
|
||||
pub mod finance;
|
||||
|
||||
@@ -43,11 +43,13 @@ icons! {
|
||||
AppWindow => "app-window",
|
||||
Archive => "archive",
|
||||
ArrowDownLeft => "arrow-down-left",
|
||||
ArrowDownToDot => "arrow-down-to-dot",
|
||||
ArrowDownToLine => "arrow-down-to-line",
|
||||
ArrowDownWideNarrow => "arrow-down-wide-narrow",
|
||||
ArrowDown => "arrow-down",
|
||||
ArrowLeft => "arrow-left",
|
||||
ArrowRight => "arrow-right",
|
||||
ArrowUpFromDot => "arrow-up-from-dot",
|
||||
ArrowUpNarrowWide => "arrow-up-narrow-wide",
|
||||
ArrowUpRight => "arrow-up-right",
|
||||
ArrowUp => "arrow-up",
|
||||
@@ -211,6 +213,7 @@ icons! {
|
||||
Puzzle => "puzzle",
|
||||
Quote => "quote",
|
||||
Redo2 => "redo-2",
|
||||
RedoDot => "redo-dot",
|
||||
RefreshCw => "refresh-cw",
|
||||
Regex => "regex",
|
||||
Reply => "reply",
|
||||
|
||||
+13
-13
@@ -379,16 +379,16 @@
|
||||
|
||||
## T20 Debug — `src/debug`
|
||||
|
||||
- [ ] DebugToolbar
|
||||
- [ ] BreakpointList
|
||||
- [ ] BreakpointGutter → `editor::EditorGutter`
|
||||
- [ ] CallStack
|
||||
- [ ] VariablesPanel
|
||||
- [ ] WatchPanel
|
||||
- [ ] ThreadList
|
||||
- [ ] MemoryViewer / HexViewer
|
||||
- [ ] Disassembly
|
||||
- [ ] PerformanceProfiler / Flamegraph
|
||||
- [ ] TimelineProfiler
|
||||
- [ ] NetworkInspector
|
||||
- [ ] ConsoleREPL → `editor::DebugConsole`
|
||||
- [x] DebugToolbar
|
||||
- [x] BreakpointList
|
||||
- [x] BreakpointGutter → `editor::CodeEditor`, its gutter and `toggle_breakpoint`
|
||||
- [x] CallStack
|
||||
- [x] VariablesPanel
|
||||
- [x] WatchPanel
|
||||
- [x] ThreadList
|
||||
- [x] MemoryViewer / HexViewer → `debug::HexViewer`, placed at `base`
|
||||
- [x] Disassembly
|
||||
- [x] PerformanceProfiler / Flamegraph → `debug::Flamegraph`
|
||||
- [x] TimelineProfiler
|
||||
- [x] NetworkInspector
|
||||
- [x] ConsoleREPL → `editor::DebugConsole`
|
||||
Reference in new issue
Block a user