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 false,
37 cx,
38 );
39
40 let div_inspector = OnceCell::new();
41 cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
42 let div_inspector = div_inspector
43 .get_or_init(|| cx.new(|cx| DivInspector::new(project.clone(), window, cx)));
44 div_inspector.update(cx, |div_inspector, cx| {
45 div_inspector.update_inspected_element(&id, state.clone(), window, cx);
46 div_inspector.render(window, cx).into_any_element()
47 })
48 });
49
50 cx.set_inspector_renderer(Box::new(render_inspector));
51}
52
53fn render_inspector(
54 inspector: &mut Inspector,
55 window: &mut Window,
56 cx: &mut Context<Inspector>,
57) -> AnyElement {
58 let ui_font = theme::setup_ui_font(window, cx);
59 let colors = cx.theme().colors();
60 let inspector_id = inspector.active_element_id();
61 let toolbar_height = PlatformTitleBar::height(window);
62
63 v_flex()
64 .size_full()
65 .bg(colors.panel_background)
66 .text_color(colors.text)
67 .font(ui_font)
68 .border_l_1()
69 .border_color(colors.border)
70 .child(
71 h_flex()
72 .justify_between()
73 .pr_2()
74 .pl_1()
75 .mt_px()
76 .h(toolbar_height)
77 .border_b_1()
78 .border_color(colors.border_variant)
79 .child(
80 IconButton::new("pick-mode", IconName::MagnifyingGlass)
81 .tooltip(Tooltip::text("Start inspector pick mode"))
82 .selected_icon_color(Color::Selected)
83 .toggle_state(inspector.is_picking())
84 .on_click(cx.listener(|inspector, _, window, _cx| {
85 inspector.start_picking();
86 window.refresh();
87 })),
88 )
89 .child(h_flex().justify_end().child(Label::new("GPUI Inspector"))),
90 )
91 .child(
92 v_flex()
93 .id("gpui-inspector-content")
94 .overflow_y_scroll()
95 .px_2()
96 .py_0p5()
97 .gap_2()
98 .when_some(inspector_id, |this, inspector_id| {
99 this.child(render_inspector_id(inspector_id, cx))
100 })
101 .children(inspector.render_inspector_states(window, cx)),
102 )
103 .into_any_element()
104}
105
106fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
107 let source_location = inspector_id.path.source_location;
108 // For unknown reasons, for some elements the path is absolute.
109 let source_location_string = source_location.to_string();
110 let source_location_string = source_location_string
111 .strip_prefix(env!("ZED_REPO_DIR"))
112 .and_then(|s| s.strip_prefix("/"))
113 .map(|s| s.to_string())
114 .unwrap_or(source_location_string);
115
116 v_flex()
117 .child(
118 h_flex()
119 .justify_between()
120 .child(Label::new("Element ID").size(LabelSize::Large))
121 .child(
122 div()
123 .id("instance-id")
124 .text_ui(cx)
125 .tooltip(Tooltip::text(
126 "Disambiguates elements from the same source location",
127 ))
128 .child(format!("Instance {}", inspector_id.instance_id)),
129 ),
130 )
131 .child(
132 div()
133 .id("source-location")
134 .text_ui(cx)
135 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
136 .underline()
137 .font_buffer(cx)
138 .text_xs()
139 .child(source_location_string)
140 .tooltip(Tooltip::text("Click to open by running Zed CLI"))
141 .on_click(move |_, _window, cx| {
142 cx.background_spawn(open_zed_source_location(source_location))
143 .detach_and_log_err(cx);
144 }),
145 )
146 .child(
147 div()
148 .id("global-id")
149 .text_ui(cx)
150 .min_h_20()
151 .tooltip(Tooltip::text(
152 "GlobalElementId of the nearest ancestor with an ID",
153 ))
154 .child(inspector_id.path.global_id.to_string()),
155 )
156}
157
158async fn open_zed_source_location(
159 location: &'static std::panic::Location<'static>,
160) -> anyhow::Result<()> {
161 let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
162 path.push(Path::new(location.file()));
163 let path_arg = format!(
164 "{}:{}:{}",
165 path.display(),
166 location.line(),
167 location.column()
168 );
169
170 let output = new_smol_command("zed")
171 .arg(&path_arg)
172 .output()
173 .await
174 .with_context(|| format!("running zed to open {path_arg} failed"))?;
175
176 if !output.status.success() {
177 Err(anyhow!(
178 "running zed to open {path_arg} failed with stderr: {}",
179 String::from_utf8_lossy(&output.stderr)
180 ))
181 } else {
182 Ok(())
183 }
184}