Add initial element inspector for Zed development (#31315)

Open inspector with `dev: toggle inspector` from command palette or
`cmd-alt-i` on mac or `ctrl-alt-i` on linux.

https://github.com/user-attachments/assets/54c43034-d40b-414e-ba9b-190bed2e6d2f

* Picking of elements via the mouse, with scroll wheel to inspect
occluded elements.

* Temporary manipulation of the selected element.

* Layout info and JSON-based style manipulation for `Div`.

* Navigation to code that constructed the element.

Big thanks to @as-cii and @maxdeviant for sorting out how to implement
the core of an inspector.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Federico Dionisi <code@fdionisi.me>
This commit is contained in:
Michael Sloan 2025-05-23 17:08:59 -06:00 committed by GitHub
parent 685933b5c8
commit ab59982bf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2631 additions and 406 deletions

View file

@ -0,0 +1,168 @@
use anyhow::{Context as _, anyhow};
use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
use std::{cell::OnceCell, path::Path, sync::Arc};
use ui::{Label, Tooltip, prelude::*};
use util::{ResultExt as _, command::new_smol_command};
use workspace::AppState;
use crate::div_inspector::DivInspector;
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
cx.on_action(|_: &zed_actions::dev::ToggleInspector, cx| {
let Some(active_window) = cx
.active_window()
.context("no active window to toggle inspector")
.log_err()
else {
return;
};
// This is deferred to avoid double lease due to window already being updated.
cx.defer(move |cx| {
active_window
.update(cx, |_, window, cx| window.toggle_inspector(cx))
.log_err();
});
});
// Project used for editor buffers + LSP support
let project = project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
cx,
);
let div_inspector = OnceCell::new();
cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
let div_inspector = div_inspector
.get_or_init(|| cx.new(|cx| DivInspector::new(project.clone(), window, cx)));
div_inspector.update(cx, |div_inspector, cx| {
div_inspector.update_inspected_element(&id, state.clone(), window, cx);
div_inspector.render(window, cx).into_any_element()
})
});
cx.set_inspector_renderer(Box::new(render_inspector));
}
fn render_inspector(
inspector: &mut Inspector,
window: &mut Window,
cx: &mut Context<Inspector>,
) -> AnyElement {
let ui_font = theme::setup_ui_font(window, cx);
let colors = cx.theme().colors();
let inspector_id = inspector.active_element_id();
v_flex()
.id("gpui-inspector")
.size_full()
.bg(colors.panel_background)
.text_color(colors.text)
.font(ui_font)
.border_l_1()
.border_color(colors.border)
.overflow_y_scroll()
.child(
h_flex()
.p_2()
.border_b_1()
.border_color(colors.border_variant)
.child(
IconButton::new("pick-mode", IconName::MagnifyingGlass)
.tooltip(Tooltip::text("Start inspector pick mode"))
.selected_icon_color(Color::Selected)
.toggle_state(inspector.is_picking())
.on_click(cx.listener(|inspector, _, window, _cx| {
inspector.start_picking();
window.refresh();
})),
)
.child(
h_flex()
.w_full()
.justify_end()
.child(Label::new("GPUI Inspector").size(LabelSize::Large)),
),
)
.child(
v_flex()
.p_2()
.gap_2()
.when_some(inspector_id, |this, inspector_id| {
this.child(render_inspector_id(inspector_id, cx))
})
.children(inspector.render_inspector_states(window, cx)),
)
.into_any_element()
}
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
let source_location = inspector_id.path.source_location;
v_flex()
.child(Label::new("Element ID").size(LabelSize::Large))
.when(inspector_id.instance_id != 0, |this| {
this.child(
div()
.id("instance-id")
.text_ui(cx)
.tooltip(Tooltip::text(
"Disambiguates elements from the same source location",
))
.child(format!("Instance {}", inspector_id.instance_id)),
)
})
.child(
div()
.id("source-location")
.text_ui(cx)
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.underline()
.child(format!("{}", source_location))
.tooltip(Tooltip::text("Click to open by running zed cli"))
.on_click(move |_, _window, cx| {
cx.background_spawn(open_zed_source_location(source_location))
.detach_and_log_err(cx);
}),
)
.child(
div()
.id("global-id")
.text_ui(cx)
.min_h_12()
.tooltip(Tooltip::text(
"GlobalElementId of the nearest ancestor with an ID",
))
.child(inspector_id.path.global_id.to_string()),
)
}
async fn open_zed_source_location(
location: &'static std::panic::Location<'static>,
) -> anyhow::Result<()> {
let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
path.push(Path::new(location.file()));
let path_arg = format!(
"{}:{}:{}",
path.display(),
location.line(),
location.column()
);
let output = new_smol_command("zed")
.arg(&path_arg)
.output()
.await
.with_context(|| format!("running zed to open {path_arg} failed"))?;
if !output.status.success() {
Err(anyhow!(
"running zed to open {path_arg} failed with stderr: {}",
String::from_utf8_lossy(&output.stderr)
))
} else {
Ok(())
}
}