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