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