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 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 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 .size_full()
61 .bg(colors.panel_background)
62 .text_color(colors.text)
63 .font(ui_font)
64 .border_l_1()
65 .border_color(colors.border)
66 .child(
67 h_flex()
68 .p_2()
69 .border_b_1()
70 .border_color(colors.border_variant)
71 .child(
72 IconButton::new("pick-mode", IconName::MagnifyingGlass)
73 .tooltip(Tooltip::text("Start inspector pick mode"))
74 .selected_icon_color(Color::Selected)
75 .toggle_state(inspector.is_picking())
76 .on_click(cx.listener(|inspector, _, window, _cx| {
77 inspector.start_picking();
78 window.refresh();
79 })),
80 )
81 .child(
82 h_flex()
83 .w_full()
84 .justify_end()
85 .child(Label::new("GPUI Inspector").size(LabelSize::Large)),
86 ),
87 )
88 .child(
89 v_flex()
90 .id("gpui-inspector-content")
91 .overflow_y_scroll()
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 // For unknown reasons, for some elements the path is absolute.
105 let source_location_string = source_location.to_string();
106 let source_location_string = source_location_string
107 .strip_prefix(env!("ZED_REPO_DIR"))
108 .and_then(|s| s.strip_prefix("/"))
109 .map(|s| s.to_string())
110 .unwrap_or(source_location_string);
111
112 v_flex()
113 .child(Label::new("Element ID").size(LabelSize::Large))
114 .child(
115 div()
116 .id("instance-id")
117 .text_ui(cx)
118 .tooltip(Tooltip::text(
119 "Disambiguates elements from the same source location",
120 ))
121 .child(format!("Instance {}", inspector_id.instance_id)),
122 )
123 .child(
124 div()
125 .id("source-location")
126 .text_ui(cx)
127 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
128 .underline()
129 .child(source_location_string)
130 .tooltip(Tooltip::text("Click to open by running zed cli"))
131 .on_click(move |_, _window, cx| {
132 cx.background_spawn(open_zed_source_location(source_location))
133 .detach_and_log_err(cx);
134 }),
135 )
136 .child(
137 div()
138 .id("global-id")
139 .text_ui(cx)
140 .min_h_20()
141 .tooltip(Tooltip::text(
142 "GlobalElementId of the nearest ancestor with an ID",
143 ))
144 .child(inspector_id.path.global_id.to_string()),
145 )
146}
147
148async fn open_zed_source_location(
149 location: &'static std::panic::Location<'static>,
150) -> anyhow::Result<()> {
151 let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
152 path.push(Path::new(location.file()));
153 let path_arg = format!(
154 "{}:{}:{}",
155 path.display(),
156 location.line(),
157 location.column()
158 );
159
160 let output = new_smol_command("zed")
161 .arg(&path_arg)
162 .output()
163 .await
164 .with_context(|| format!("running zed to open {path_arg} failed"))?;
165
166 if !output.status.success() {
167 Err(anyhow!(
168 "running zed to open {path_arg} failed with stderr: {}",
169 String::from_utf8_lossy(&output.stderr)
170 ))
171 } else {
172 Ok(())
173 }
174}