1use std::collections::{BTreeMap, VecDeque};
2
3#[cfg(feature = "dev-reload")]
4use gpui::{
5 AnyElement, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled,
6 deferred, div, px, rgba,
7};
8
9use crate::{
10 ComponentRegistry, ExecutionTiming, PrimitiveValue, SourceLocation, StateInstanceSnapshot,
11 StoreSnapshot, StyleProperties, ThemeVariant, UiNode, UiNodeKind, UiRuntimeState, UiValue,
12};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum RuntimeTraceKind {
16 Reconcile,
17 Event,
18 Action,
19 Capability,
20 Task,
21 Subscription,
22 State,
23 Store,
24 Signal,
25 Theme,
26 Locale,
27 Reload,
28 Window,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct RuntimeTrace {
33 pub sequence: u64,
34 pub kind: RuntimeTraceKind,
35 pub scope: String,
36 pub message: String,
37 pub payload: Option<UiValue>,
38 pub sensitive: bool,
39}
40
41#[derive(Clone, Debug)]
42pub struct TraceBuffer {
43 capacity: usize,
44 next_sequence: u64,
45 traces: VecDeque<RuntimeTrace>,
46}
47
48impl Default for TraceBuffer {
49 fn default() -> Self {
50 Self::new(200)
51 }
52}
53
54impl TraceBuffer {
55 #[must_use]
56 pub fn new(capacity: usize) -> Self {
57 Self {
58 capacity: capacity.max(1),
59 next_sequence: 1,
60 traces: VecDeque::new(),
61 }
62 }
63
64 pub fn push(
65 &mut self,
66 kind: RuntimeTraceKind,
67 scope: impl Into<String>,
68 message: impl Into<String>,
69 payload: Option<UiValue>,
70 sensitive: bool,
71 ) {
72 if self.traces.len() == self.capacity {
73 self.traces.pop_front();
74 }
75 self.traces.push_back(RuntimeTrace {
76 sequence: self.next_sequence,
77 kind,
78 scope: scope.into(),
79 message: message.into(),
80 payload: if sensitive && payload.is_some() {
81 Some(UiValue::String("<sensitive>".to_owned()))
82 } else {
83 payload
84 },
85 sensitive,
86 });
87 self.next_sequence = self.next_sequence.saturating_add(1);
88 }
89
90 #[must_use]
91 pub fn snapshot(&self) -> Vec<RuntimeTrace> {
92 self.traces.iter().cloned().collect()
93 }
94}
95
96#[derive(Clone, Debug, PartialEq)]
97pub struct InspectorNode {
98 pub path: String,
99 pub kind: String,
100 pub key: Option<String>,
101 pub source: Option<SourceLocation>,
102 pub props: BTreeMap<String, String>,
103 pub attributes: BTreeMap<String, String>,
104 pub handlers: Vec<String>,
105 pub motions: Vec<String>,
106 pub style: StyleProperties,
107 pub children: Vec<InspectorNode>,
108}
109
110#[derive(Clone, Debug)]
111pub struct InspectorSnapshot {
112 pub root: Option<InspectorNode>,
113 pub state: Vec<StateInstanceSnapshot>,
114 pub stores: Vec<StoreSnapshot>,
115 pub theme_family: String,
116 pub theme_name: String,
117 pub theme_colors: BTreeMap<String, String>,
118 pub traces: Vec<RuntimeTrace>,
119 pub timings: Vec<ExecutionTiming>,
120 pub dirty: Vec<String>,
121 pub components: Vec<InspectorComponent>,
122 pub signals: Vec<InspectorSignal>,
123 pub effects: Vec<InspectorEffect>,
124 pub motions: Vec<InspectorMotion>,
125 pub timelines: Vec<InspectorTimeline>,
126 pub timers: Vec<InspectorTimer>,
127 pub element_refs: Vec<InspectorElementRef>,
128 pub virtual_collections: Vec<InspectorVirtualCollection>,
129 pub geometry_nodes: usize,
130 pub pointer_captures: BTreeMap<u64, u64>,
131 pub locale_readers: usize,
132 pub theme_readers: usize,
133 pub viewport_readers: usize,
134 pub clock_elapsed_ms: u64,
135}
136
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct InspectorComponent {
139 pub id: String,
140 pub export: String,
141 pub props: BTreeMap<String, String>,
142 pub slots: Vec<String>,
143 pub parts: Vec<String>,
144}
145
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct InspectorSignal {
148 pub id: String,
149 pub kind: String,
150 pub value: String,
151 pub revision: u64,
152 pub last_writer: String,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq)]
156pub struct InspectorEffect {
157 pub id: String,
158 pub activation: u64,
159 pub dependencies: String,
160 pub start: String,
161 pub cleanup: String,
162 pub generation: u64,
163}
164
165#[derive(Clone, Debug, PartialEq)]
166pub struct InspectorMotion {
167 pub id: String,
168 pub kind: String,
169 pub value: f64,
170 pub target: f64,
171 pub velocity: Option<f64>,
172 pub elapsed_ms: u64,
173 pub duration_ms: Option<u64>,
174 pub repeating: bool,
175 pub active: bool,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct InspectorTimeline {
180 pub id: String,
181 pub state: String,
182 pub elapsed_ms: u64,
183 pub duration_ms: Option<u64>,
184 pub iterations: Option<u32>,
185 pub autoreverse: bool,
186 pub track_count: usize,
187}
188
189#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct InspectorTimer {
191 pub id: String,
192 pub delay_ms: u64,
193 pub remaining_ms: u64,
194 pub declaration_paused: bool,
195 pub interaction_paused: bool,
196 pub view_paused: bool,
197 pub callback: String,
198 pub generation: u64,
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct InspectorElementRef {
203 pub id: String,
204 pub node: u64,
205}
206
207#[derive(Clone, Debug, PartialEq)]
208pub struct InspectorVirtualCollection {
209 pub id: String,
210 pub item_count: usize,
211 pub realized_count: usize,
212 pub realized_range: std::ops::Range<usize>,
213 pub requested_count: usize,
214 pub requested_range: std::ops::Range<usize>,
215 pub visible_range: std::ops::Range<usize>,
216 pub viewport_height: f64,
217 pub scroll_item: usize,
218 pub scroll_offset: f64,
219 pub is_scrolled: bool,
220 pub sticky_header: Option<usize>,
221 pub bottom_align: bool,
222 pub follow_tail: bool,
223}
224
225struct InspectorRuntimeMechanisms {
226 signals: Vec<InspectorSignal>,
227 effects: Vec<InspectorEffect>,
228 motions: Vec<InspectorMotion>,
229 timelines: Vec<InspectorTimeline>,
230 timers: Vec<InspectorTimer>,
231 element_refs: Vec<InspectorElementRef>,
232 virtual_collections: Vec<InspectorVirtualCollection>,
233 geometry_nodes: usize,
234 pointer_captures: BTreeMap<u64, u64>,
235 locale_readers: usize,
236 theme_readers: usize,
237 viewport_readers: usize,
238 clock_elapsed_ms: u64,
239}
240
241impl InspectorSnapshot {
242 #[must_use]
243 pub fn capture(
244 root: Option<&UiNode>,
245 runtime: &UiRuntimeState,
246 theme: &ThemeVariant,
247 components: &ComponentRegistry,
248 timings: Vec<ExecutionTiming>,
249 ) -> Self {
250 Self::capture_for_view(root, runtime, theme, components, timings, None)
251 }
252
253 #[must_use]
254 pub fn capture_for_view(
255 root: Option<&UiNode>,
256 runtime: &UiRuntimeState,
257 theme: &ThemeVariant,
258 components: &ComponentRegistry,
259 timings: Vec<ExecutionTiming>,
260 view: Option<&str>,
261 ) -> Self {
262 let mechanisms = inspect_runtime_mechanisms(runtime, view);
263 Self {
264 root: root.map(|root| inspect_node(root, "root")),
265 state: runtime.component_state.inspect(),
266 stores: runtime.stores.inspect(),
267 theme_family: theme.family.clone(),
268 theme_name: theme.name.clone(),
269 theme_colors: theme
270 .tokens
271 .colors
272 .iter()
273 .map(|(name, color)| (name.clone(), format!("#{:08x}", color.as_rgba_hex())))
274 .collect(),
275 traces: runtime.traces.snapshot(),
276 timings,
277 dirty: runtime
278 .dirty_components()
279 .iter()
280 .map(ToString::to_string)
281 .collect(),
282 components: components
283 .iter()
284 .map(|(id, component)| InspectorComponent {
285 id: id.to_string(),
286 export: component.metadata.export.clone(),
287 props: component
288 .schema
289 .props
290 .iter()
291 .map(|(name, field)| {
292 (
293 name.clone(),
294 if field.sensitive {
295 "<sensitive>".to_owned()
296 } else {
297 format!("{:?}", field.schema)
298 },
299 )
300 })
301 .collect(),
302 slots: component.schema.slots.keys().cloned().collect(),
303 parts: component.schema.parts.iter().cloned().collect(),
304 })
305 .collect(),
306 signals: mechanisms.signals,
307 effects: mechanisms.effects,
308 motions: mechanisms.motions,
309 timelines: mechanisms.timelines,
310 timers: mechanisms.timers,
311 element_refs: mechanisms.element_refs,
312 virtual_collections: mechanisms.virtual_collections,
313 geometry_nodes: mechanisms.geometry_nodes,
314 pointer_captures: mechanisms.pointer_captures,
315 locale_readers: mechanisms.locale_readers,
316 theme_readers: mechanisms.theme_readers,
317 viewport_readers: mechanisms.viewport_readers,
318 clock_elapsed_ms: mechanisms.clock_elapsed_ms,
319 }
320 }
321}
322
323#[allow(clippy::too_many_lines)]
324fn inspect_runtime_mechanisms(
325 runtime: &UiRuntimeState,
326 view: Option<&str>,
327) -> InspectorRuntimeMechanisms {
328 let geometry = runtime.geometry_for(view);
329 let pointer_capture = runtime.pointer_capture_for(view);
330 InspectorRuntimeMechanisms {
331 signals: runtime
332 .signals
333 .inspect()
334 .into_iter()
335 .map(|signal| InspectorSignal {
336 id: format!(
337 "{}/{}:{}",
338 signal.id.component(),
339 signal.id.key(),
340 signal.id.kind().as_str()
341 ),
342 kind: signal.id.kind().as_str().to_owned(),
343 value: format!("{:?}", signal.value),
344 revision: signal.revision,
345 last_writer: format!("{:?}", signal.last_writer),
346 })
347 .collect(),
348 effects: runtime
349 .effects
350 .iter_active()
351 .map(|(id, effect, activation)| InspectorEffect {
352 id: format!("{}/{}", id.component(), id.key()),
353 activation,
354 dependencies: display_value(effect.dependencies(), false),
355 start: effect.start().name().to_owned(),
356 cleanup: effect.cleanup().name().to_owned(),
357 generation: effect.generation().get(),
358 })
359 .collect(),
360 motions: runtime
361 .motions
362 .inspect(runtime.clock.now())
363 .into_iter()
364 .map(|animation| InspectorMotion {
365 id: format!(
366 "{}/{}",
367 animation.key.component,
368 animation.key.property.as_str()
369 ),
370 kind: animation.kind,
371 value: animation.value,
372 target: animation.target,
373 velocity: animation.velocity,
374 elapsed_ms: animation.elapsed_ms,
375 duration_ms: animation.duration_ms,
376 repeating: animation.repeating,
377 active: animation.active,
378 })
379 .collect(),
380 timelines: runtime
381 .motions
382 .inspect_timelines(runtime.clock.now())
383 .into_iter()
384 .map(|timeline| InspectorTimeline {
385 id: format!(
386 "{}/{}/{}",
387 timeline.handle.domain(),
388 timeline.handle.owner(),
389 timeline.handle.name()
390 ),
391 state: format!("{:?}", timeline.state),
392 elapsed_ms: timeline.elapsed_ms,
393 duration_ms: timeline.duration_ms,
394 iterations: timeline.iterations,
395 autoreverse: timeline.autoreverse,
396 track_count: timeline.track_count,
397 })
398 .collect(),
399 timers: runtime
400 .timers
401 .inspect(runtime.clock.now())
402 .into_iter()
403 .map(|timer| InspectorTimer {
404 id: format!("{}/{}", timer.id.component(), timer.id.key()),
405 delay_ms: u64::try_from(timer.delay.as_millis()).unwrap_or(u64::MAX),
406 remaining_ms: u64::try_from(timer.remaining.as_millis()).unwrap_or(u64::MAX),
407 declaration_paused: timer.declaration_paused,
408 interaction_paused: timer.interaction_paused,
409 view_paused: timer.view_paused,
410 callback: timer.callback,
411 generation: timer.generation.get(),
412 })
413 .collect(),
414 element_refs: runtime
415 .element_refs
416 .iter()
417 .map(|(id, node)| InspectorElementRef {
418 id: format!("{}/{}", id.component(), id.key()),
419 node: node.get(),
420 })
421 .collect(),
422 virtual_collections: inspect_virtual_collections(runtime),
423 geometry_nodes: geometry.len(),
424 pointer_captures: pointer_capture
425 .snapshot()
426 .into_iter()
427 .map(|(pointer, node)| (pointer, node.get()))
428 .collect(),
429 locale_readers: runtime.environment_dependencies.locale_reader_count(),
430 theme_readers: runtime.environment_dependencies.theme_reader_count(),
431 viewport_readers: runtime.environment_dependencies.viewport_reader_count(),
432 clock_elapsed_ms: u64::try_from(runtime.clock.elapsed().as_millis()).unwrap_or(u64::MAX),
433 }
434}
435
436fn inspect_virtual_collections(runtime: &UiRuntimeState) -> Vec<InspectorVirtualCollection> {
437 runtime
438 .virtual_requests
439 .inspect()
440 .into_iter()
441 .map(|metrics| InspectorVirtualCollection {
442 id: format!("{}/{}", metrics.id.component, metrics.id.key),
443 item_count: metrics.item_count,
444 realized_count: metrics.realized_count,
445 realized_range: metrics.realized_range,
446 requested_count: metrics.requested_count,
447 requested_range: metrics.requested_range,
448 visible_range: metrics.visible_range,
449 viewport_height: metrics.viewport_height,
450 scroll_item: metrics.scroll_item,
451 scroll_offset: metrics.scroll_offset,
452 is_scrolled: metrics.is_scrolled,
453 sticky_header: metrics.sticky_header,
454 bottom_align: metrics.bottom_align,
455 follow_tail: metrics.follow_tail,
456 })
457 .collect()
458}
459
460fn inspect_node(node: &UiNode, path: &str) -> InspectorNode {
461 let mut props = BTreeMap::new();
462 let (kind, descendants): (String, Vec<&UiNode>) = match node.kind() {
463 UiNodeKind::Text { text } => {
464 props.insert("text".to_owned(), truncate(text, 80));
465 ("text".to_owned(), Vec::new())
466 }
467 UiNodeKind::RichText { text, spans } => {
468 (inspect_rich_text(text, spans, &mut props), Vec::new())
469 }
470 UiNodeKind::Canvas { scene } => (inspect_canvas(scene, &mut props), Vec::new()),
471 UiNodeKind::Svg { source } => {
472 props.insert("bytes".to_owned(), source.len().to_string());
473 ("svg".to_owned(), Vec::new())
474 }
475 UiNodeKind::Box { children } => ("box".to_owned(), children.iter().collect()),
476 UiNodeKind::Fragment { children } => ("fragment".to_owned(), children.iter().collect()),
477 UiNodeKind::Custom { primitive } => {
478 props = inspect_primitive_props(primitive);
479 (
480 format!("primitive:{}", primitive.primitive.as_str()),
481 Vec::new(),
482 )
483 }
484 UiNodeKind::Image { source } => inspect_image(source, &mut props),
485 UiNodeKind::DirectionalImage {
486 left_to_right,
487 right_to_left,
488 } => inspect_directional_image(left_to_right, right_to_left, &mut props),
489 UiNodeKind::Overlay {
490 trigger,
491 content,
492 spec,
493 } => {
494 props.insert("id".to_owned(), spec.id.as_str().to_owned());
495 props.insert("kind".to_owned(), format!("{:?}", spec.kind));
496 props.insert("placement".to_owned(), format!("{:?}", spec.placement));
497 props.insert("open".to_owned(), spec.open.to_string());
498 (
499 "overlay".to_owned(),
500 vec![trigger.as_ref(), content.as_ref()],
501 )
502 }
503 UiNodeKind::Layer { content, spec } => {
504 props.insert("id".to_owned(), spec.id.as_str().to_owned());
505 props.insert("placement".to_owned(), format!("{:?}", spec.placement));
506 props.insert("priority".to_owned(), spec.priority.to_string());
507 ("layer".to_owned(), vec![content.as_ref()])
508 }
509 UiNodeKind::VirtualCollection { spec } => inspect_virtual_collection(spec, &mut props),
510 UiNodeKind::ErrorBoundary { child, fallback } => (
511 "error_boundary".to_owned(),
512 vec![child.as_ref(), fallback.as_ref()],
513 ),
514 };
515 let children = descendants
516 .into_iter()
517 .enumerate()
518 .map(|(index, child)| {
519 let segment = child
520 .key()
521 .map_or_else(|| index.to_string(), |key| format!("key:{}", key.as_str()));
522 inspect_node(child, &format!("{path}/{segment}"))
523 })
524 .collect();
525 InspectorNode {
526 path: path.to_owned(),
527 kind,
528 key: node.key().map(|key| key.as_str().to_owned()),
529 source: node.source().cloned(),
530 props,
531 attributes: node
532 .attributes()
533 .iter()
534 .map(|(name, value)| (name.clone(), display_value(value, false)))
535 .collect(),
536 handlers: node
537 .handlers()
538 .iter()
539 .flat_map(|(event, bindings)| {
540 bindings.iter().map(move |binding| {
541 format!(
542 "{event}:{:?}={}",
543 binding.phase(),
544 binding.handler().diagnostic_label()
545 )
546 })
547 })
548 .collect(),
549 motions: node
550 .motions()
551 .iter()
552 .map(|animation| format!("{animation:?}"))
553 .collect(),
554 style: node.style().resolve(&crate::InteractionState::default()),
555 children,
556 }
557}
558
559fn inspect_primitive_props(primitive: &crate::PrimitiveNode) -> BTreeMap<String, String> {
560 let props = primitive
561 .props
562 .iter()
563 .map(|(name, value)| (name.to_owned(), primitive_value(value)))
564 .collect();
565 #[cfg(feature = "charts")]
566 {
567 let mut props = props;
568 if primitive.primitive.as_str() == "gpui_rhai.chart" {
569 inspect_chart_primitive(primitive, &mut props);
570 }
571 props
572 }
573 #[cfg(not(feature = "charts"))]
574 {
575 props
576 }
577}
578
579#[cfg(feature = "charts")]
580fn inspect_chart_primitive(primitive: &crate::PrimitiveNode, props: &mut BTreeMap<String, String>) {
581 let Some(crate::PrimitiveValue::Data(spec)) = primitive.props.get("spec") else {
582 return;
583 };
584 let spec = match crate::ChartSpec::from_ui_value(spec) {
585 Ok(spec) => spec,
586 Err(error) => {
587 props.insert("chart_config_error".to_owned(), error.to_string());
588 return;
589 }
590 };
591 let lengths = match primitive.props.get("data") {
592 Some(crate::PrimitiveValue::ChartData(data)) => data
593 .snapshot()
594 .datasets()
595 .map(|(name, dataset)| (name.to_owned(), dataset.len()))
596 .collect::<BTreeMap<_, _>>(),
597 Some(crate::PrimitiveValue::Data(crate::UiValue::Array(rows))) => {
598 BTreeMap::from([("main".to_owned(), rows.len())])
599 }
600 _ => BTreeMap::new(),
601 };
602 let sampling = spec
603 .series
604 .iter()
605 .filter_map(|series| {
606 let source = lengths.get(&series.dataset).copied()?;
607 let explicit = series
608 .transforms
609 .iter()
610 .find_map(|transform| match transform {
611 crate::ChartTransformSpec::Downsample { threshold, .. } => Some(*threshold),
612 _ => None,
613 });
614 let target = explicit.or_else(|| {
615 matches!(
616 series.kind,
617 crate::ChartSeriesKind::Line | crate::ChartSeriesKind::Area
618 )
619 .then_some(4_000)
620 })?;
621 (source > target).then(|| format!("{}:{target}/{source}", series.key))
622 })
623 .collect::<Vec<_>>();
624 if !sampling.is_empty() {
625 props.insert("chart_sampling_plan".to_owned(), sampling.join(", "));
626 }
627}
628
629fn inspect_virtual_collection<'a>(
630 spec: &'a crate::VirtualCollectionNodeSpec,
631 props: &mut BTreeMap<String, String>,
632) -> (String, Vec<&'a UiNode>) {
633 props.insert("items".to_owned(), spec.data.len().to_string());
634 props.insert("realized".to_owned(), spec.realized.len().to_string());
635 (
636 "virtual_collection".to_owned(),
637 spec.realized.values().collect(),
638 )
639}
640
641fn inspect_rich_text(
642 text: &str,
643 spans: &[crate::Span],
644 props: &mut BTreeMap<String, String>,
645) -> String {
646 props.insert("text".to_owned(), truncate(text, 80));
647 props.insert("span_count".to_owned(), spans.len().to_string());
648 "rich_text".to_owned()
649}
650
651fn inspect_canvas(scene: &crate::CanvasScene, props: &mut BTreeMap<String, String>) -> String {
652 props.insert(
653 "command_count".to_owned(),
654 scene.commands().len().to_string(),
655 );
656 "canvas".to_owned()
657}
658
659fn inspect_image<'a>(
660 source: &crate::ImageSourceSpec,
661 props: &mut BTreeMap<String, String>,
662) -> (String, Vec<&'a UiNode>) {
663 props.insert("source".to_owned(), inspect_image_source(source));
664 ("image".to_owned(), Vec::new())
665}
666
667fn inspect_directional_image<'a>(
668 left_to_right: &crate::ImageSourceSpec,
669 right_to_left: &crate::ImageSourceSpec,
670 props: &mut BTreeMap<String, String>,
671) -> (String, Vec<&'a UiNode>) {
672 props.insert(
673 "left_to_right".to_owned(),
674 inspect_image_source(left_to_right),
675 );
676 props.insert(
677 "right_to_left".to_owned(),
678 inspect_image_source(right_to_left),
679 );
680 ("directional_image".to_owned(), Vec::new())
681}
682
683fn inspect_image_source(source: &crate::ImageSourceSpec) -> String {
684 match source {
685 crate::ImageSourceSpec::Handle(handle) => format!("{}#<opaque>", handle.kind()),
686 crate::ImageSourceSpec::Asset(asset) => asset.as_str().to_owned(),
687 }
688}
689
690fn primitive_value(value: &PrimitiveValue) -> String {
691 match value {
692 PrimitiveValue::Data(value) => display_value(value, false),
693 PrimitiveValue::Node(_) => "<node>".to_owned(),
694 PrimitiveValue::Nodes(nodes) => format!("<{} nodes>", nodes.len()),
695 PrimitiveValue::Callback(callback) => callback.diagnostic_label(),
696 PrimitiveValue::Style(_) => "<style>".to_owned(),
697 PrimitiveValue::Length(length) => format!("{length:?}"),
698 PrimitiveValue::Asset(asset) => asset.as_str().to_owned(),
699 PrimitiveValue::Signal(signal) => format!("signal:{}", signal.id().key()),
700 PrimitiveValue::Ref(reference) => format!("ref:{}", reference.id().key()),
701 PrimitiveValue::Document(document) => format!(
702 "{}@{} ({} bytes)",
703 document.identity(),
704 document.revision(),
705 document.text().len()
706 ),
707 #[cfg(feature = "charts")]
708 PrimitiveValue::ChartData(data) => format!("chart-data@{}", data.revision()),
709 }
710}
711
712fn display_value(value: &UiValue, sensitive: bool) -> String {
713 if sensitive {
714 return "<redacted>".to_owned();
715 }
716 match value {
717 UiValue::Null => "null".to_owned(),
718 UiValue::Bool(value) => value.to_string(),
719 UiValue::Integer(value) => value.to_string(),
720 UiValue::Float(value) => value.to_string(),
721 UiValue::String(value) => format!("{:?}", truncate(value, 80)),
722 UiValue::Array(values) => format!("[{} items]", values.len()),
723 UiValue::Map(values) => format!("{{{} fields}}", values.len()),
724 UiValue::Handle(handle) => format!("{}#<opaque>", handle.kind()),
725 }
726}
727
728fn truncate(value: &str, max: usize) -> String {
729 let mut output = value.chars().take(max).collect::<String>();
730 if value.chars().count() > max {
731 output.push('…');
732 }
733 output
734}
735
736#[must_use]
737#[cfg(feature = "dev-reload")]
738pub(crate) fn inspector_element(snapshot: &InspectorSnapshot) -> AnyElement {
739 let mut lines = inspector_header(snapshot);
740 if let Some(root) = &snapshot.root {
741 append_node_lines(root, 0, &mut lines);
742 }
743 lines.push("Components".to_owned());
744 for component in &snapshot.components {
745 lines.push(format!(" {}::{}", component.id, component.export));
746 lines.push(format!(" props={:?}", component.props));
747 lines.push(format!(" slots={:?}", component.slots));
748 lines.push(format!(" parts={:?}", component.parts));
749 }
750 append_mechanism_lines(snapshot, &mut lines);
751 lines.push("State".to_owned());
752 for instance in &snapshot.state {
753 lines.push(format!(" {}", instance.path));
754 for (name, value) in &instance.fields {
755 lines.push(format!(
756 " {name}: {}",
757 display_value(&value.value, value.sensitive)
758 ));
759 }
760 }
761 lines.push("Stores".to_owned());
762 for store in &snapshot.stores {
763 lines.push(format!(" {:?}:{}", store.id.scope, store.id.name));
764 for (name, value) in &store.fields {
765 lines.push(format!(
766 " {name}: {}",
767 display_value(&value.value, value.sensitive)
768 ));
769 }
770 }
771 lines.push("Theme tokens".to_owned());
772 for (name, value) in &snapshot.theme_colors {
773 lines.push(format!(" {name}: {value}"));
774 }
775 lines.push("Invalidations".to_owned());
776 if snapshot.dirty.is_empty() {
777 lines.push(" none pending".to_owned());
778 } else {
779 lines.extend(snapshot.dirty.iter().map(|path| format!(" dirty: {path}")));
780 }
781 lines.push("Script timings".to_owned());
782 for timing in snapshot.timings.iter().rev().take(20) {
783 lines.push(format!(
784 " {:?} {} {:.2?} {} ops(v{}){}",
785 timing.operation,
786 timing.source,
787 timing.duration,
788 timing.operations,
789 timing.operation_semantics,
790 if timing.slow { " SLOW" } else { "" }
791 ));
792 }
793 lines.push("Recent traces".to_owned());
794 for trace in snapshot.traces.iter().rev().take(20) {
795 lines.push(format!(
796 " #{} {:?} {} — {} {}",
797 trace.sequence,
798 trace.kind,
799 trace.scope,
800 trace.message,
801 trace
802 .payload
803 .as_ref()
804 .map_or_else(String::new, |value| display_value(value, trace.sensitive))
805 ));
806 }
807 deferred(
808 div()
809 .id("gpui-rhai-inspector")
810 .absolute()
811 .top_0()
812 .right_0()
813 .w(px(420.0))
814 .h_full()
815 .overflow_y_scroll()
816 .restrict_scroll_to_axis()
817 .p_3()
818 .bg(rgba(0x1717_1bee))
819 .text_color(rgba(0xf4f4_f5ff))
820 .text_size(px(11.0))
821 .children(lines.into_iter().map(|line| div().child(line))),
822 )
823 .with_priority(10_000)
824 .into_any_element()
825}
826
827#[cfg(feature = "dev-reload")]
828fn inspector_header(snapshot: &InspectorSnapshot) -> Vec<String> {
829 vec![
830 format!(
831 "GPUI Rhai Inspector — {} / {}",
832 snapshot.theme_family, snapshot.theme_name
833 ),
834 format!(
835 "nodes={} state={} stores={} signals={} effects={} motions={} timelines={} timers={} refs={} virtual={} geometry={} captures={} dirty={} traces={} timings={} clock={}ms",
836 snapshot.root.as_ref().map_or(0, count_nodes),
837 snapshot.state.len(),
838 snapshot.stores.len(),
839 snapshot.signals.len(),
840 snapshot.effects.len(),
841 snapshot.motions.len(),
842 snapshot.timelines.len(),
843 snapshot.timers.len(),
844 snapshot.element_refs.len(),
845 snapshot.virtual_collections.len(),
846 snapshot.geometry_nodes,
847 snapshot.pointer_captures.len(),
848 snapshot.dirty.len(),
849 snapshot.traces.len(),
850 snapshot.timings.len(),
851 snapshot.clock_elapsed_ms
852 ),
853 ]
854}
855
856#[cfg(feature = "dev-reload")]
857fn append_mechanism_lines(snapshot: &InspectorSnapshot, lines: &mut Vec<String>) {
858 lines.push(format!(
859 "Dependencies — locale={} theme={} viewport={}",
860 snapshot.locale_readers, snapshot.theme_readers, snapshot.viewport_readers
861 ));
862 lines.push("Signals".to_owned());
863 for signal in &snapshot.signals {
864 lines.push(format!(
865 " {} {}={} rev={} writer={}",
866 signal.id, signal.kind, signal.value, signal.revision, signal.last_writer
867 ));
868 }
869 lines.push("Effects".to_owned());
870 for effect in &snapshot.effects {
871 lines.push(format!(
872 " {} activation={} deps={} start={} cleanup={} gen={}",
873 effect.id,
874 effect.activation,
875 effect.dependencies,
876 effect.start,
877 effect.cleanup,
878 effect.generation
879 ));
880 }
881 lines.push("Motion sources".to_owned());
882 for motion in &snapshot.motions {
883 lines.push(format!(
884 " {} {} value={:.4} target={:.4} velocity={:?} elapsed={}ms duration={:?} repeat={} active={}",
885 motion.id,
886 motion.kind,
887 motion.value,
888 motion.target,
889 motion.velocity,
890 motion.elapsed_ms,
891 motion.duration_ms,
892 motion.repeating,
893 motion.active
894 ));
895 }
896 lines.push("Motion timelines".to_owned());
897 for timeline in &snapshot.timelines {
898 lines.push(format!(
899 " {} state={} elapsed={}ms duration={:?} iterations={:?} reverse={} tracks={}",
900 timeline.id,
901 timeline.state,
902 timeline.elapsed_ms,
903 timeline.duration_ms,
904 timeline.iterations,
905 timeline.autoreverse,
906 timeline.track_count
907 ));
908 }
909 lines.push("Timers".to_owned());
910 for timer in &snapshot.timers {
911 lines.push(format!(
912 " {} remaining={}ms/{}ms paused={}/{}/{} callback={} gen={}",
913 timer.id,
914 timer.remaining_ms,
915 timer.delay_ms,
916 timer.declaration_paused,
917 timer.interaction_paused,
918 timer.view_paused,
919 timer.callback,
920 timer.generation
921 ));
922 }
923 lines.push("Element refs".to_owned());
924 for reference in &snapshot.element_refs {
925 lines.push(format!(" {} -> node {}", reference.id, reference.node));
926 }
927 lines.push("Virtual collections".to_owned());
928 for collection in &snapshot.virtual_collections {
929 lines.push(format!(
930 " {} items={} realized={}:{:?} requested={}:{:?} visible={:?} viewport={:.1} top={}:{} sticky={:?} scrolled={} bottom={} follow_tail={}",
931 collection.id,
932 collection.item_count,
933 collection.realized_count,
934 collection.realized_range,
935 collection.requested_count,
936 collection.requested_range,
937 collection.visible_range,
938 collection.viewport_height,
939 collection.scroll_item,
940 collection.scroll_offset,
941 collection.sticky_header,
942 collection.is_scrolled,
943 collection.bottom_align,
944 collection.follow_tail
945 ));
946 }
947 if !snapshot.pointer_captures.is_empty() {
948 lines.push(format!("Pointer captures: {:?}", snapshot.pointer_captures));
949 }
950}
951
952#[cfg(feature = "dev-reload")]
953fn count_nodes(node: &InspectorNode) -> usize {
954 1 + node.children.iter().map(count_nodes).sum::<usize>()
955}
956
957#[cfg(feature = "dev-reload")]
958fn append_node_lines(node: &InspectorNode, depth: usize, lines: &mut Vec<String>) {
959 let source = node.source.as_ref().map_or_else(String::new, |source| {
960 format!(" @ {}:{}:{}", source.module, source.line, source.column)
961 });
962 lines.push(format!(
963 "{}{} {}{}",
964 " ".repeat(depth),
965 node.path,
966 node.kind,
967 source
968 ));
969 let indent = " ".repeat(depth.saturating_add(1));
970 if let Some(key) = &node.key {
971 lines.push(format!("{indent}key={key}"));
972 }
973 if !node.props.is_empty() {
974 lines.push(format!("{indent}props={:?}", node.props));
975 }
976 if !node.attributes.is_empty() {
977 lines.push(format!("{indent}attributes={:?}", node.attributes));
978 }
979 if !node.handlers.is_empty() {
980 lines.push(format!("{indent}handlers={:?}", node.handlers));
981 }
982 if !node.motions.is_empty() {
983 lines.push(format!("{indent}motions={:?}", node.motions));
984 }
985 lines.push(format!("{indent}computed_style={:?}", node.style));
986 for child in &node.children {
987 append_node_lines(child, depth.saturating_add(1), lines);
988 }
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::{
995 ComponentInstancePath, ComponentStateSchema, EventPropagation, HostCallback, StateField,
996 ValueSchema,
997 };
998 use std::time::{Duration, Instant};
999
1000 #[test]
1001 fn inspector_reports_host_handler_label_without_opaque_details() {
1002 let root = UiNode::text("Host").with_host_handler(
1003 "click",
1004 HostCallback::new("widget.select", |_, _, _| EventPropagation::Handled),
1005 );
1006 let inspected = inspect_node(&root, "root");
1007 assert_eq!(inspected.handlers, vec!["click:Target=host:widget.select"]);
1008 }
1009
1010 #[test]
1011 fn snapshot_captures_source_and_redacts_sensitive_state() {
1012 let mut runtime = UiRuntimeState::new();
1013 let path = ComponentInstancePath::root("Login", "root");
1014 let schema = ComponentStateSchema::new(BTreeMap::from([(
1015 "token".to_owned(),
1016 StateField::new(ValueSchema::string(), UiValue::String("secret".to_owned()))
1017 .sensitive(true),
1018 )]))
1019 .unwrap();
1020 let mut transaction = runtime.component_state.begin_render();
1021 transaction.mount(path, &schema).unwrap();
1022 runtime.component_state.commit_render(transaction);
1023 let root = UiNode::text("Login").with_source(SourceLocation {
1024 module: "ui/main.rhai".to_owned(),
1025 line: 4,
1026 column: 9,
1027 });
1028 let engine = crate::RuntimeEngine::new();
1029 let theme = crate::load_theme_source(
1030 engine.engine(),
1031 "theme.rhai",
1032 include_str!("../../../registry/themes/default_dark.rhai"),
1033 )
1034 .unwrap();
1035 let snapshot = InspectorSnapshot::capture(
1036 Some(&root),
1037 &runtime,
1038 &theme,
1039 &ComponentRegistry::default(),
1040 Vec::new(),
1041 );
1042 assert_eq!(snapshot.root.unwrap().source.unwrap().line, 4);
1043 assert!(snapshot.state[0].fields["token"].sensitive);
1044 assert_eq!(
1045 snapshot.state[0].fields["token"].value,
1046 UiValue::String("<sensitive>".to_owned())
1047 );
1048 assert_eq!(
1049 display_value(&snapshot.state[0].fields["token"].value, true),
1050 "<redacted>"
1051 );
1052 }
1053
1054 #[test]
1055 fn trace_buffer_is_bounded_and_ordered() {
1056 let mut traces = TraceBuffer::new(2);
1057 for index in 0..3 {
1058 traces.push(
1059 RuntimeTraceKind::Event,
1060 "root",
1061 format!("event-{index}"),
1062 None,
1063 false,
1064 );
1065 }
1066 let snapshot = traces.snapshot();
1067 assert_eq!(snapshot.len(), 2);
1068 assert_eq!(snapshot[0].sequence, 2);
1069 assert_eq!(snapshot[1].sequence, 3);
1070 }
1071
1072 #[test]
1073 fn sensitive_trace_payload_is_redacted_before_storage() {
1074 let mut traces = TraceBuffer::new(1);
1075 traces.push(
1076 RuntimeTraceKind::State,
1077 "login",
1078 "token changed",
1079 Some(UiValue::String("secret".to_owned())),
1080 true,
1081 );
1082 assert_eq!(
1083 traces.snapshot()[0].payload,
1084 Some(UiValue::String("<sensitive>".to_owned()))
1085 );
1086 }
1087
1088 fn install_virtual_metrics_probe(runtime: &UiRuntimeState, component: &ComponentInstancePath) {
1089 runtime.virtual_requests.request(
1090 crate::VirtualCollectionId {
1091 component: component.clone(),
1092 key: "rows".to_owned(),
1093 },
1094 [4, 5],
1095 );
1096 }
1097
1098 #[test]
1099 fn snapshot_includes_runtime_mechanism_identity_and_state() {
1100 let component = ComponentInstancePath::root("View", "main");
1101 let mut runtime = UiRuntimeState::new();
1102 let now = Instant::now();
1103 let clock = crate::ManualRuntimeClock::new(now);
1104 runtime.clock = clock.clock();
1105 let signal_id =
1106 crate::SignalId::new(component.clone(), "progress", crate::SignalKind::Float).unwrap();
1107 runtime.signals.reconcile(
1108 &component,
1109 BTreeMap::from([(
1110 signal_id.clone(),
1111 crate::signal::SignalDescriptor::new(crate::SignalValue::Float(0.25)),
1112 )]),
1113 );
1114 let signal = crate::NativeSignal::new(signal_id);
1115 runtime
1116 .signals
1117 .write_from(
1118 &signal,
1119 crate::SignalValue::Float(0.5),
1120 crate::SignalWriter::Script,
1121 )
1122 .unwrap();
1123
1124 let mut callback = crate::ScriptCallback::try_from_fn_ptr(
1125 rhai::FnPtr::new("fired").unwrap(),
1126 crate::ScriptGeneration::initial(),
1127 )
1128 .unwrap();
1129 callback.bind_component_if_unset(component.clone(), BTreeMap::new());
1130 let timer = crate::TimerDescriptor::new(
1131 crate::TimerId::new(component.clone(), "refresh").unwrap(),
1132 Duration::from_secs(1),
1133 false,
1134 callback,
1135 UiValue::Null,
1136 )
1137 .unwrap();
1138 runtime.timers.reconcile(
1139 &component,
1140 BTreeMap::from([(timer.id().clone(), timer)]),
1141 now,
1142 );
1143 let mut transition =
1144 crate::MotionTransition::new(crate::MotionProperty::Opacity, 0.0, 1.0, 1_000);
1145 transition.easing = crate::MotionEasing::Linear;
1146 runtime
1147 .motions
1148 .start(
1149 component.clone(),
1150 crate::MotionSource::Transition(transition),
1151 now,
1152 )
1153 .unwrap();
1154 clock.advance(Duration::from_millis(250));
1155
1156 let mut retained = crate::RetainedUiTree::new();
1157 retained.reconcile(UiNode::text("target")).unwrap();
1158 let node = retained.root_id().unwrap();
1159 let reference = crate::ElementRefId::new(component.clone(), "target").unwrap();
1160 runtime
1161 .element_refs
1162 .reconcile(&component, BTreeMap::from([(reference, node)]));
1163 runtime.pointer_capture.capture(7, node);
1164 let bounds = crate::GeometryBounds::new(0.0, 0.0, 10.0, 10.0).unwrap();
1165 runtime.geometry.update(
1166 node,
1167 crate::ElementGeometry {
1168 layout: bounds,
1169 visual: bounds,
1170 clip: None,
1171 },
1172 );
1173 install_virtual_metrics_probe(&runtime, &component);
1174
1175 let engine = crate::RuntimeEngine::new();
1176 let theme = crate::load_theme_source(
1177 engine.engine(),
1178 "theme.rhai",
1179 include_str!("../../../registry/themes/default_dark.rhai"),
1180 )
1181 .unwrap();
1182 let snapshot = InspectorSnapshot::capture(
1183 retained.root(),
1184 &runtime,
1185 &theme,
1186 &ComponentRegistry::default(),
1187 Vec::new(),
1188 );
1189 assert_eq!(snapshot.signals[0].revision, 1);
1190 assert_eq!(snapshot.signals[0].last_writer, "Script");
1191 assert_eq!(snapshot.timers[0].callback, "fired");
1192 assert_eq!(snapshot.clock_elapsed_ms, 250);
1193 assert_eq!(snapshot.motions[0].kind, "transition");
1194 assert!((snapshot.motions[0].value - 0.25).abs() < f64::EPSILON);
1195 assert_eq!(snapshot.motions[0].duration_ms, Some(1_000));
1196 assert_eq!(snapshot.element_refs[0].node, node.get());
1197 assert_eq!(snapshot.virtual_collections[0].requested_range, 4..6);
1198 assert_eq!(snapshot.geometry_nodes, 1);
1199 assert_eq!(snapshot.pointer_captures.get(&7), Some(&node.get()));
1200 }
1201}