1use anyhow::{Context as _, anyhow};
2use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
3use std::{cell::OnceCell, path::Path, sync::Arc};
4use ui::{Label, Tooltip, prelude::*};
5use util::{ResultExt as _, command::new_smol_command};
6use workspace::AppState;
7
8use crate::div_inspector::DivInspector;
9
10pub fn init(app_state: Arc<AppState>, cx: &mut App) {
11 cx.on_action(|_: &zed_actions::dev::ToggleInspector, cx| {
12 let Some(active_window) = cx
13 .active_window()
14 .context("no active window to toggle inspector")
15 .log_err()
16 else {
17 return;
18 };
19 // This is deferred to avoid double lease due to window already being updated.
20 cx.defer(move |cx| {
21 active_window
22 .update(cx, |_, window, cx| window.toggle_inspector(cx))
23 .log_err();
24 });
25 });
26
27 // Project used for editor buffers + LSP support
28 let project = project::Project::local(
29 app_state.client.clone(),
30 app_state.node_runtime.clone(),
31 app_state.user_store.clone(),
32 app_state.languages.clone(),
33 app_state.fs.clone(),
34 None,
35 cx,
36 );
37
38 let div_inspector = OnceCell::new();
39 cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
40 let div_inspector = div_inspector
41 .get_or_init(|| cx.new(|cx| DivInspector::new(project.clone(), window, cx)));
42 div_inspector.update(cx, |div_inspector, cx| {
43 div_inspector.update_inspected_element(&id, state.clone(), window, cx);
44 div_inspector.render(window, cx).into_any_element()
45 })
46 });
47
48 cx.set_inspector_renderer(Box::new(render_inspector));
49}
50
51fn render_inspector(
52 inspector: &mut Inspector,
53 window: &mut Window,
54 cx: &mut Context<Inspector>,
55) -> AnyElement {
56 let ui_font = theme::setup_ui_font(window, cx);
57 let colors = cx.theme().colors();
58 let inspector_id = inspector.active_element_id();
59 v_flex()
60 .id("gpui-inspector")
61 .size_full()
62 .bg(colors.panel_background)
63 .text_color(colors.text)
64 .font(ui_font)
65 .border_l_1()
66 .border_color(colors.border)
67 .overflow_y_scroll()
68 .child(
69 h_flex()
70 .p_2()
71 .border_b_1()
72 .border_color(colors.border_variant)
73 .child(
74 IconButton::new("pick-mode", IconName::MagnifyingGlass)
75 .tooltip(Tooltip::text("Start inspector pick mode"))
76 .selected_icon_color(Color::Selected)
77 .toggle_state(inspector.is_picking())
78 .on_click(cx.listener(|inspector, _, window, _cx| {
79 inspector.start_picking();
80 window.refresh();
81 })),
82 )
83 .child(
84 h_flex()
85 .w_full()
86 .justify_end()
87 .child(Label::new("GPUI Inspector").size(LabelSize::Large)),
88 ),
89 )
90 .child(
91 v_flex()
92 .p_2()
93 .gap_2()
94 .when_some(inspector_id, |this, inspector_id| {
95 this.child(render_inspector_id(inspector_id, cx))
96 })
97 .children(inspector.render_inspector_states(window, cx)),
98 )
99 .into_any_element()
100}
101
102fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
103 let source_location = inspector_id.path.source_location;
104 v_flex()
105 .child(Label::new("Element ID").size(LabelSize::Large))
106 .when(inspector_id.instance_id != 0, |this| {
107 this.child(
108 div()
109 .id("instance-id")
110 .text_ui(cx)
111 .tooltip(Tooltip::text(
112 "Disambiguates elements from the same source location",
113 ))
114 .child(format!("Instance {}", inspector_id.instance_id)),
115 )
116 })
117 .child(
118 div()
119 .id("source-location")
120 .text_ui(cx)
121 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
122 .underline()
123 .child(format!("{}", source_location))
124 .tooltip(Tooltip::text("Click to open by running zed cli"))
125 .on_click(move |_, _window, cx| {
126 cx.background_spawn(open_zed_source_location(source_location))
127 .detach_and_log_err(cx);
128 }),
129 )
130 .child(
131 div()
132 .id("global-id")
133 .text_ui(cx)
134 .min_h_12()
135 .tooltip(Tooltip::text(
136 "GlobalElementId of the nearest ancestor with an ID",
137 ))
138 .child(inspector_id.path.global_id.to_string()),
139 )
140}
141
142async fn open_zed_source_location(
143 location: &'static std::panic::Location<'static>,
144) -> anyhow::Result<()> {
145 let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
146 path.push(Path::new(location.file()));
147 let path_arg = format!(
148 "{}:{}:{}",
149 path.display(),
150 location.line(),
151 location.column()
152 );
153
154 let output = new_smol_command("zed")
155 .arg(&path_arg)
156 .output()
157 .await
158 .with_context(|| format!("running zed to open {path_arg} failed"))?;
159
160 if !output.status.success() {
161 Err(anyhow!(
162 "running zed to open {path_arg} failed with stderr: {}",
163 String::from_utf8_lossy(&output.stderr)
164 ))
165 } else {
166 Ok(())
167 }
168}