T20: Debug
- DebugToolbar, BreakpointList, CallStack with folded library frames, ThreadList - VariablesPanel and WatchPanel, colored by kind, changes washed - HexViewer at a base address with the byte under the pointer lit on both sides - Disassembly with source lines, registers and numbers colored, gutter breakpoints - Flamegraph that zooms by frame, TimelineProfiler that zooms and pans by wheel - NetworkInspector with a waterfall, a filter and a request's headers and body - icons: redo-dot, arrow-down-to-dot, arrow-up-from-dot - gallery: the Debug page
This commit is contained in:
1 parent
2c961833ac
commit
f8bfa29c17
22 files changed
+3240
-14
No files matched your search
@@ -0,0 +1,344 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, Entity, FontWeight, Hsla, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
forms::{Input, TextInput},
|
||||
primitives::{Disclosure, IconName},
|
||||
theme::{ActiveTheme, ControlSize, Palette, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
|
||||
/// What a value is, which picks its color.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ValueKind {
|
||||
Text,
|
||||
Number,
|
||||
Bool,
|
||||
Null,
|
||||
Object,
|
||||
List,
|
||||
Function,
|
||||
}
|
||||
|
||||
impl ValueKind {
|
||||
fn color(self, colors: &Palette) -> Hsla {
|
||||
let syntax = &colors.syntax;
|
||||
match self {
|
||||
Self::Text => syntax.string,
|
||||
Self::Number => syntax.number,
|
||||
Self::Bool | Self::Null => syntax.keyword,
|
||||
Self::Object | Self::List => syntax.type_name,
|
||||
Self::Function => syntax.function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A variable: its name, its value as the debugger prints it, its kind and type, and its fields or items.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Variable {
|
||||
pub name: SharedString,
|
||||
pub value: SharedString,
|
||||
pub kind: ValueKind,
|
||||
pub type_name: Option<SharedString>,
|
||||
pub children: Vec<Variable>,
|
||||
}
|
||||
|
||||
/// Rows for `variables` under `path`, their children when open: each row's key, depth and variable.
|
||||
pub(crate) fn flat<'a>(
|
||||
variables: &'a [Variable],
|
||||
path: &str,
|
||||
depth: usize,
|
||||
open: &[SharedString],
|
||||
out: &mut Vec<(SharedString, usize, &'a Variable)>,
|
||||
) {
|
||||
for variable in variables {
|
||||
let key: SharedString = format!("{path}/{}", variable.name).into();
|
||||
let opened = open.contains(&key);
|
||||
out.push((key.clone(), depth, variable));
|
||||
if opened {
|
||||
flat(&variable.children, &key, depth + 1, open, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OnToggle = Rc<dyn Fn(&SharedString, bool, &mut Window, &mut App)>;
|
||||
|
||||
/// Variables by scope, as a tree: each with its value colored by kind and its type, fields and items opening under it; values that changed at the last step washed.
|
||||
#[derive(IntoElement)]
|
||||
pub struct VariablesPanel {
|
||||
id: ElementId,
|
||||
scopes: Vec<(SharedString, Vec<Variable>)>,
|
||||
open: Vec<SharedString>,
|
||||
changed: Vec<SharedString>,
|
||||
on_toggle: Option<OnToggle>,
|
||||
}
|
||||
|
||||
impl VariablesPanel {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
scopes: impl IntoIterator<Item = (SharedString, Vec<Variable>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
scopes: scopes.into_iter().collect(),
|
||||
open: Vec::new(),
|
||||
changed: Vec::new(),
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scopes and variables shown open, by key: `scope`, `scope/name`, `scope/name/field`.
|
||||
pub fn open(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
|
||||
self.open = keys.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Variables whose values changed at the last step, by key.
|
||||
pub fn changed(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
|
||||
self.changed = keys.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for VariablesPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let toggle_row = |key: SharedString, opens: bool, opened: bool, depth: usize| {
|
||||
let toggle = self.on_toggle.clone();
|
||||
div()
|
||||
.id((self.id.clone(), key.clone()))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.pl(theme.text_size(TextSize::Sm) * (depth as f32 * 1.1 + 0.4))
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when(opens, |row| {
|
||||
row.cursor_pointer().when_some(toggle, |row, toggle| {
|
||||
row.on_click(move |_, window, cx| toggle(&key, !opened, window, cx))
|
||||
})
|
||||
})
|
||||
.child(div().flex_none().w_4().children(opens.then(|| {
|
||||
Disclosure::new(
|
||||
(self.id.clone(), format!("chevron-{depth}-{opened}")),
|
||||
opened,
|
||||
)
|
||||
})))
|
||||
};
|
||||
let mut rows: Vec<AnyElement> = Vec::new();
|
||||
for (scope, variables) in &self.scopes {
|
||||
let scope_key = scope.clone();
|
||||
let opened = self.open.contains(&scope_key);
|
||||
rows.push(
|
||||
toggle_row(scope_key.clone(), true, opened, 0)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(colors.fg_muted)
|
||||
.child(scope.clone())
|
||||
.into_any_element(),
|
||||
);
|
||||
if !opened {
|
||||
continue;
|
||||
}
|
||||
let mut shown = Vec::new();
|
||||
flat(variables, scope, 1, &self.open, &mut shown);
|
||||
for (key, depth, variable) in shown {
|
||||
let opens = !variable.children.is_empty();
|
||||
let changed = self.changed.contains(&key);
|
||||
rows.push(
|
||||
toggle_row(key.clone(), opens, self.open.contains(&key), depth)
|
||||
.font_family(theme.mono_family.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.syntax.property)
|
||||
.child(variable.name.clone()),
|
||||
)
|
||||
.child(div().flex_none().text_color(colors.fg_subtle).child(":"))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.px_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.when(changed, |value| value.bg(colors.warning.opacity(0.18)))
|
||||
.text_color(variable.kind.color(&colors))
|
||||
.child(Ellipsis::new(variable.value.clone())),
|
||||
)
|
||||
.children(
|
||||
variable.type_name.clone().map(|kind| {
|
||||
div().flex_none().text_color(colors.fg_subtle).child(kind)
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
}
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// A watched expression and what it gives: a value and its kind, or the error it raised.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Watch {
|
||||
pub expression: SharedString,
|
||||
pub value: Result<(SharedString, ValueKind), SharedString>,
|
||||
}
|
||||
|
||||
/// Expressions watched at every step, each with its value or its error; a field adds one, and each removes.
|
||||
#[derive(IntoElement)]
|
||||
pub struct WatchPanel {
|
||||
id: ElementId,
|
||||
watches: Vec<Watch>,
|
||||
field: Entity<TextInput>,
|
||||
on_remove: Option<OnIndex>,
|
||||
}
|
||||
|
||||
impl WatchPanel {
|
||||
/// `field` takes a new expression; its owner adds it on Enter.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
watches: impl IntoIterator<Item = Watch>,
|
||||
field: &Entity<TextInput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
watches: watches.into_iter().collect(),
|
||||
field: field.clone(),
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_remove = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WatchPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(self.watches.iter().enumerate().map(|(ix, watch)| {
|
||||
let group = SharedString::from(format!("watch-{ix}"));
|
||||
let remove = self.on_remove.clone();
|
||||
let (value, color) = match &watch.value {
|
||||
Ok((value, kind)) => (value.clone(), kind.color(&colors)),
|
||||
Err(error) => (error.clone(), colors.danger),
|
||||
};
|
||||
div()
|
||||
.id((self.id.clone(), format!("row-{ix}")))
|
||||
.group(group.clone())
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
.font_family(theme.mono_family.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.fg)
|
||||
.child(watch.expression.clone()),
|
||||
)
|
||||
.child(div().flex_none().text_color(colors.fg_subtle).child("="))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_color(color)
|
||||
.child(Ellipsis::new(value)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.)
|
||||
.group_hover(group, |actions| actions.opacity(1.))
|
||||
.child(
|
||||
IconButton::new(
|
||||
(self.id.clone(), format!("remove-{ix}")),
|
||||
IconName::X,
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Stop watching")
|
||||
.when_some(
|
||||
remove,
|
||||
|button, remove| {
|
||||
button.on_click(move |_, window, cx| remove(ix, window, cx))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}))
|
||||
.child(Input::new(&self.field).size(ControlSize::Sm))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn variable(name: &str, children: Vec<Variable>) -> Variable {
|
||||
Variable {
|
||||
name: name.to_string().into(),
|
||||
value: "".into(),
|
||||
kind: ValueKind::Object,
|
||||
type_name: None,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_variables_show_their_fields_under_them() {
|
||||
let all = [
|
||||
variable(
|
||||
"palette",
|
||||
vec![variable("colors", vec![variable("accent", vec![])])],
|
||||
),
|
||||
variable("count", vec![]),
|
||||
];
|
||||
let mut rows = Vec::new();
|
||||
flat(&all, "Locals", 1, &["Locals/palette".into()], &mut rows);
|
||||
let keys: Vec<(&str, usize)> = rows
|
||||
.iter()
|
||||
.map(|(key, depth, _)| (key.as_ref(), *depth))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
keys,
|
||||
[
|
||||
("Locals/palette", 1),
|
||||
("Locals/palette/colors", 2),
|
||||
("Locals/count", 1)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user