Add a live Rust style editor to inspector to edit a sequence of no-argument style modifiers (#31443)

Editing JSON styles is not very helpful for bringing style changes back
to the actual code. This PR adds a buffer that pretends to be Rust,
applying any style attribute identifiers it finds. Also supports
completions with display of documentation. The effect of the currently
selected completion is previewed. Warning diagnostics appear on any
unrecognized identifier.


https://github.com/user-attachments/assets/af39ff0a-26a5-4835-a052-d8f642b2080c

Adds a `#[derive_inspector_reflection]` macro which allows these methods
to be enumerated and called by their name. The macro code changes were
95% generated by Zed Agent + Opus 4.

Release Notes:

* Added an element inspector for development. On debug builds,
`dev::ToggleInspector` will open a pane allowing inspecting of element
info and modifying styles.
This commit is contained in:
Michael Sloan 2025-05-26 11:43:57 -06:00 committed by GitHub
parent 6253b95f82
commit 649072d140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1778 additions and 316 deletions

View file

@ -15,6 +15,7 @@ path = "src/inspector_ui.rs"
anyhow.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
@ -23,6 +24,6 @@ serde_json_lenient.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View file

@ -1,8 +1,6 @@
# Inspector
This is a tool for inspecting and manipulating rendered elements in Zed. It is
only available in debug builds. Use the `dev::ToggleInspector` action to toggle
inspector mode and click on UI elements to inspect them.
This is a tool for inspecting and manipulating rendered elements in Zed. It is only available in debug builds. Use the `dev::ToggleInspector` action to toggle inspector mode and click on UI elements to inspect them.
# Current features
@ -10,44 +8,72 @@ inspector mode and click on UI elements to inspect them.
* Temporary manipulation of the selected element.
* Layout info and JSON-based style manipulation for `Div`.
* Layout info for `Div`.
* Both Rust and JSON-based style manipulation of `Div` style. The rust style editor only supports argumentless `Styled` and `StyledExt` method calls.
* Navigation to code that constructed the element.
# Known bugs
* The style inspector buffer will leak memory over time due to building up
history on each change of inspected element. Instead of using `Project` to
create it, should just directly build the `Buffer` and `File` each time the inspected element changes.
## JSON style editor undo history doesn't get reset
The JSON style editor appends to its undo stack on every change of the active inspected element.
I attempted to fix it by creating a new buffer and setting the buffer associated with the `json_style_buffer` entity. Unfortunately this doesn't work because the language server uses the `version: clock::Global` to figure out the changes, so would need some way to start the new buffer's text at that version.
```
json_style_buffer.update(cx, |json_style_buffer, cx| {
let language = json_style_buffer.language().cloned();
let file = json_style_buffer.file().cloned();
*json_style_buffer = Buffer::local("", cx);
json_style_buffer.set_language(language, cx);
if let Some(file) = file {
json_style_buffer.file_updated(file, cx);
}
});
```
# Future features
* Info and manipulation of element types other than `Div`.
* Action and keybinding for entering pick mode.
* Ability to highlight current element after it's been picked.
* Info and manipulation of element types other than `Div`.
* Indicate when the picked element has disappeared.
* To inspect elements that disappear, it would be helpful to be able to pause the UI.
* Hierarchy view?
## Better manipulation than JSON
## Methods that take arguments in Rust style editor
The current approach is not easy to move back to the code. Possibilities:
Could use TreeSitter to parse out the fluent style method chain and arguments. Tricky part of this is completions - ideally the Rust Analyzer already being used by the developer's Zed would be used.
* Editable list of style attributes to apply.
## Edit original code in Rust style editor
* Rust buffer of code that does a very lenient parse to get the style attributes. Some options:
Two approaches:
- Take all the identifier-like tokens and use them if they are the name of an attribute. A custom completion provider in a buffer could be used.
1. Open an excerpt of the original file.
- Use TreeSitter to parse out the fluent style method chain. With this approach the buffer could even be the actual code file. Tricky part of this is LSP - ideally the LSP already being used by the developer's Zed would be used.
2. Communicate with the Zed process that has the repo open - it would send the code for the element. This seems like a lot of work, but would be very nice for rapid development, and it would allow use of rust analyzer.
## Source locations
With both approaches, would need to record the buffer version and use that when referring to source locations, since editing elements can cause code layout shift.
## Source location UI improvements
* Mode to navigate to source code on every element change while picking.
* Tracking of more source locations - currently the source location is often in a ui compoenent. Ideally this would have a way for the components to indicate that they are probably not the source location the user is looking for.
- Could have `InspectorElementId` be `Vec<(ElementId, Option<Location>)>`, but if there are multiple code paths that construct the same element this would cause them to be considered different.
- Probably better to have a separate `Vec<Option<Location>>` that uses the same indices as `GlobalElementId`.
## Persistent modification
Currently, element modifications disappear when picker mode is started. Handling this well is tricky. Potential features:
@ -60,9 +86,11 @@ Currently, element modifications disappear when picker mode is started. Handling
* The code should probably distinguish the data that is provided by the element and the modifications from the inspector. Currently these are conflated in element states.
If support is added for editing original code, then the logical selector in this case would be just matches of the source path.
# Code cleanups
## Remove special side pane rendering
## Consider removing special side pane rendering
Currently the inspector has special rendering in the UI, but maybe it could just be a workspace item.

View file

@ -1,26 +1,64 @@
use anyhow::Result;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
use anyhow::{Result, anyhow};
use editor::{Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MultiBuffer};
use fuzzy::StringMatch;
use gpui::{
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
Window,
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
};
use language::Buffer;
use language::language_settings::SoftWrap;
use project::{Project, ProjectPath};
use language::{
Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
};
use project::lsp_store::CompletionDocumentation;
use project::{Completion, CompletionSource, Project, ProjectPath};
use std::cell::RefCell;
use std::fmt::Write as _;
use std::ops::Range;
use std::path::Path;
use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
use std::rc::Rc;
use std::sync::LazyLock;
use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this
/// matches the name used in the generated schemas.
const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
pub(crate) struct DivInspector {
state: State,
project: Entity<Project>,
inspector_id: Option<InspectorElementId>,
state: Option<DivInspectorState>,
style_buffer: Option<Entity<Buffer>>,
style_editor: Option<Entity<Editor>>,
last_error: Option<SharedString>,
inspector_state: Option<DivInspectorState>,
/// Value of `DivInspectorState.base_style` when initially picked.
initial_style: StyleRefinement,
/// Portion of `initial_style` that can't be converted to rust code.
unconvertible_style: StyleRefinement,
/// Edits the user has made to the json buffer: `json_editor - (unconvertible_style + rust_editor)`.
json_style_overrides: StyleRefinement,
/// Error to display from parsing the json, or if serialization errors somehow occur.
json_style_error: Option<SharedString>,
/// Currently selected completion.
rust_completion: Option<String>,
/// Range that will be replaced by the completion if selected.
rust_completion_replace_range: Option<Range<Anchor>>,
}
enum State {
Loading,
BuffersLoaded {
rust_style_buffer: Entity<Buffer>,
json_style_buffer: Entity<Buffer>,
},
Ready {
rust_style_buffer: Entity<Buffer>,
rust_style_editor: Entity<Editor>,
json_style_buffer: Entity<Buffer>,
json_style_editor: Entity<Editor>,
},
LoadError {
message: SharedString,
},
}
impl DivInspector {
@ -29,32 +67,402 @@ impl DivInspector {
window: &mut Window,
cx: &mut Context<Self>,
) -> DivInspector {
// Open the buffer once, so it can then be used for each editor.
// Open the buffers once, so they can then be used for each editor.
cx.spawn_in(window, {
let languages = project.read(cx).languages().clone();
let project = project.clone();
async move |this, cx| Self::open_style_buffer(project, this, cx).await
async move |this, cx| {
// Open the JSON style buffer in the inspector-specific project, so that it runs the
// JSON language server.
let json_style_buffer =
Self::create_buffer_in_project(ZED_INSPECTOR_STYLE_JSON, &project, cx).await;
// Create Rust style buffer without adding it to the project / buffer_store, so that
// Rust Analyzer doesn't get started for it.
let rust_language_result = languages.language_for_name("Rust").await;
let rust_style_buffer = rust_language_result.and_then(|rust_language| {
cx.new(|cx| Buffer::local("", cx).with_language(rust_language, cx))
});
match json_style_buffer.and_then(|json_style_buffer| {
rust_style_buffer
.map(|rust_style_buffer| (json_style_buffer, rust_style_buffer))
}) {
Ok((json_style_buffer, rust_style_buffer)) => {
this.update_in(cx, |this, window, cx| {
this.state = State::BuffersLoaded {
json_style_buffer: json_style_buffer,
rust_style_buffer: rust_style_buffer,
};
// Initialize editors immediately instead of waiting for
// `update_inspected_element`. This avoids continuing to show
// "Loading..." until the user moves the mouse to a different element.
if let Some(id) = this.inspector_id.take() {
let inspector_state =
window.with_inspector_state(Some(&id), cx, |state, _window| {
state.clone()
});
if let Some(inspector_state) = inspector_state {
this.update_inspected_element(&id, inspector_state, window, cx);
cx.notify();
}
}
})
.ok();
}
Err(err) => {
this.update(cx, |this, _cx| {
this.state = State::LoadError {
message: format!(
"Failed to create buffers for style editing: {err}"
)
.into(),
};
})
.ok();
}
}
}
})
.detach();
DivInspector {
state: State::Loading,
project,
inspector_id: None,
state: None,
style_buffer: None,
style_editor: None,
last_error: None,
inspector_state: None,
initial_style: StyleRefinement::default(),
unconvertible_style: StyleRefinement::default(),
json_style_overrides: StyleRefinement::default(),
rust_completion: None,
rust_completion_replace_range: None,
json_style_error: None,
}
}
async fn open_style_buffer(
project: Entity<Project>,
this: WeakEntity<DivInspector>,
pub fn update_inspected_element(
&mut self,
id: &InspectorElementId,
inspector_state: DivInspectorState,
window: &mut Window,
cx: &mut Context<Self>,
) {
let style = (*inspector_state.base_style).clone();
self.inspector_state = Some(inspector_state);
if self.inspector_id.as_ref() == Some(id) {
return;
}
self.inspector_id = Some(id.clone());
self.initial_style = style.clone();
let (rust_style_buffer, json_style_buffer) = match &self.state {
State::BuffersLoaded {
rust_style_buffer,
json_style_buffer,
}
| State::Ready {
rust_style_buffer,
json_style_buffer,
..
} => (rust_style_buffer.clone(), json_style_buffer.clone()),
State::Loading | State::LoadError { .. } => return,
};
let json_style_editor = self.create_editor(json_style_buffer.clone(), window, cx);
let rust_style_editor = self.create_editor(rust_style_buffer.clone(), window, cx);
rust_style_editor.update(cx, {
let div_inspector = cx.entity();
|rust_style_editor, _cx| {
rust_style_editor.set_completion_provider(Some(Rc::new(
RustStyleCompletionProvider { div_inspector },
)));
}
});
let rust_style = match self.reset_style_editors(&rust_style_buffer, &json_style_buffer, cx)
{
Ok(rust_style) => {
self.json_style_error = None;
rust_style
}
Err(err) => {
self.json_style_error = Some(format!("{err}").into());
return;
}
};
cx.subscribe_in(&json_style_editor, window, {
let id = id.clone();
let rust_style_buffer = rust_style_buffer.clone();
move |this, editor, event: &EditorEvent, window, cx| match event {
EditorEvent::BufferEdited => {
let style_json = editor.read(cx).text(cx);
match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
Ok(new_style) => {
let (rust_style, _) = this.style_from_rust_buffer_snapshot(
&rust_style_buffer.read(cx).snapshot(),
);
let mut unconvertible_plus_rust = this.unconvertible_style.clone();
unconvertible_plus_rust.refine(&rust_style);
// The serialization of `DefiniteLength::Fraction` does not perfectly
// roundtrip because with f32, `(x / 100.0 * 100.0) == x` is not always
// true (such as for `p_1_3`). This can cause these values to
// erroneously appear in `json_style_overrides` since they are not
// perfectly equal. Roundtripping before `subtract` fixes this.
unconvertible_plus_rust =
serde_json::to_string(&unconvertible_plus_rust)
.ok()
.and_then(|json| {
serde_json_lenient::from_str_lenient(&json).ok()
})
.unwrap_or(unconvertible_plus_rust);
this.json_style_overrides =
new_style.subtract(&unconvertible_plus_rust);
window.with_inspector_state::<DivInspectorState, _>(
Some(&id),
cx,
|inspector_state, _window| {
if let Some(inspector_state) = inspector_state.as_mut() {
*inspector_state.base_style = new_style;
}
},
);
window.refresh();
this.json_style_error = None;
}
Err(err) => this.json_style_error = Some(err.to_string().into()),
}
}
_ => {}
}
})
.detach();
cx.subscribe(&rust_style_editor, {
let json_style_buffer = json_style_buffer.clone();
let rust_style_buffer = rust_style_buffer.clone();
move |this, _editor, event: &EditorEvent, cx| match event {
EditorEvent::BufferEdited => {
this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
}
_ => {}
}
})
.detach();
self.unconvertible_style = style.subtract(&rust_style);
self.json_style_overrides = StyleRefinement::default();
self.state = State::Ready {
rust_style_buffer,
rust_style_editor,
json_style_buffer,
json_style_editor,
};
}
fn reset_style(&mut self, cx: &mut App) {
match &self.state {
State::Ready {
rust_style_buffer,
json_style_buffer,
..
} => {
if let Err(err) = self.reset_style_editors(
&rust_style_buffer.clone(),
&json_style_buffer.clone(),
cx,
) {
self.json_style_error = Some(format!("{err}").into());
} else {
self.json_style_error = None;
}
}
_ => {}
}
}
fn reset_style_editors(
&self,
rust_style_buffer: &Entity<Buffer>,
json_style_buffer: &Entity<Buffer>,
cx: &mut App,
) -> Result<StyleRefinement> {
let json_text = match serde_json::to_string_pretty(&self.initial_style) {
Ok(json_text) => json_text,
Err(err) => {
return Err(anyhow!("Failed to convert style to JSON: {err}"));
}
};
let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style);
rust_style_buffer.update(cx, |rust_style_buffer, cx| {
rust_style_buffer.set_text(rust_code, cx);
let snapshot = rust_style_buffer.snapshot();
let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
Self::set_rust_buffer_diagnostics(
unrecognized_ranges,
rust_style_buffer,
&snapshot,
cx,
);
});
json_style_buffer.update(cx, |json_style_buffer, cx| {
json_style_buffer.set_text(json_text, cx);
});
Ok(rust_style)
}
fn handle_rust_completion_selection_change(
&mut self,
rust_completion: Option<String>,
cx: &mut Context<Self>,
) {
self.rust_completion = rust_completion;
if let State::Ready {
rust_style_buffer,
json_style_buffer,
..
} = &self.state
{
self.update_json_style_from_rust(
&json_style_buffer.clone(),
&rust_style_buffer.clone(),
cx,
);
}
}
fn update_json_style_from_rust(
&mut self,
json_style_buffer: &Entity<Buffer>,
rust_style_buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) {
let rust_style = rust_style_buffer.update(cx, |rust_style_buffer, cx| {
let snapshot = rust_style_buffer.snapshot();
let (rust_style, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
Self::set_rust_buffer_diagnostics(
unrecognized_ranges,
rust_style_buffer,
&snapshot,
cx,
);
rust_style
});
// Preserve parts of the json style which do not come from the unconvertible style or rust
// style. This way user edits to the json style are preserved when they are not overridden
// by the rust style.
//
// This results in a behavior where user changes to the json style that do overlap with the
// rust style will get set to the rust style when the user edits the rust style. It would be
// possible to update the rust style when the json style changes, but this is undesirable
// as the user may be working on the actual code in the rust style.
let mut new_style = self.unconvertible_style.clone();
new_style.refine(&self.json_style_overrides);
let new_style = new_style.refined(rust_style);
match serde_json::to_string_pretty(&new_style) {
Ok(json) => {
json_style_buffer.update(cx, |json_style_buffer, cx| {
json_style_buffer.set_text(json, cx);
});
}
Err(err) => {
self.json_style_error = Some(err.to_string().into());
}
}
}
fn style_from_rust_buffer_snapshot(
&self,
snapshot: &BufferSnapshot,
) -> (StyleRefinement, Vec<Range<Anchor>>) {
let method_names = if let Some((completion, completion_range)) = self
.rust_completion
.as_ref()
.zip(self.rust_completion_replace_range.as_ref())
{
let before_text = snapshot
.text_for_range(0..completion_range.start.to_offset(&snapshot))
.collect::<String>();
let after_text = snapshot
.text_for_range(
completion_range.end.to_offset(&snapshot)
..snapshot.clip_offset(usize::MAX, Bias::Left),
)
.collect::<String>();
let mut method_names = split_str_with_ranges(&before_text, is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string()))
.collect::<Vec<_>>();
method_names.push((None, completion.clone()));
method_names.extend(
split_str_with_ranges(&after_text, is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string())),
);
method_names
} else {
split_str_with_ranges(&snapshot.text(), is_not_identifier_char)
.into_iter()
.map(|(range, name)| (Some(range), name.to_string()))
.collect::<Vec<_>>()
};
let mut style = StyleRefinement::default();
let mut unrecognized_ranges = Vec::new();
for (range, name) in method_names {
if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
style = method.invoke(style);
} else if let Some(range) = range {
unrecognized_ranges
.push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
}
}
(style, unrecognized_ranges)
}
fn set_rust_buffer_diagnostics(
unrecognized_ranges: Vec<Range<Anchor>>,
rust_style_buffer: &mut Buffer,
snapshot: &BufferSnapshot,
cx: &mut Context<Buffer>,
) {
let diagnostic_entries = unrecognized_ranges
.into_iter()
.enumerate()
.map(|(ix, range)| DiagnosticEntry {
range,
diagnostic: Diagnostic {
message: "unrecognized".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
group_id: ix,
..Default::default()
},
});
let diagnostics = DiagnosticSet::from_sorted_entries(diagnostic_entries, snapshot);
rust_style_buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
}
async fn create_buffer_in_project(
path: impl AsRef<Path>,
project: &Entity<Project>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
) -> Result<Entity<Buffer>> {
let worktree = project
.update(cx, |project, cx| {
project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
})?
.update(cx, |project, cx| project.create_worktree(path, false, cx))?
.await?;
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
@ -62,66 +470,22 @@ impl DivInspector {
path: Path::new("").into(),
})?;
let style_buffer = project
let buffer = project
.update(cx, |project, cx| project.open_path(project_path, cx))?
.await?
.1;
project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&style_buffer, cx)
})?;
this.update_in(cx, |this, window, cx| {
this.style_buffer = Some(style_buffer);
if let Some(id) = this.inspector_id.clone() {
let state =
window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
if let Some(state) = state {
this.update_inspected_element(&id, state, window, cx);
cx.notify();
}
}
})?;
Ok(())
Ok(buffer)
}
pub fn update_inspected_element(
&mut self,
id: &InspectorElementId,
state: DivInspectorState,
fn create_editor(
&self,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let base_style_json = serde_json::to_string_pretty(&state.base_style);
self.state = Some(state);
if self.inspector_id.as_ref() == Some(id) {
return;
} else {
self.inspector_id = Some(id.clone());
}
let Some(style_buffer) = self.style_buffer.clone() else {
return;
};
let base_style_json = match base_style_json {
Ok(base_style_json) => base_style_json,
Err(err) => {
self.style_editor = None;
self.last_error =
Some(format!("Failed to convert base_style to JSON: {err}").into());
return;
}
};
self.last_error = None;
style_buffer.update(cx, |style_buffer, cx| {
style_buffer.set_text(base_style_json, cx)
});
let style_editor = cx.new(|cx| {
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
) -> Entity<Editor> {
cx.new(|cx| {
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
EditorMode::full(),
multi_buffer,
@ -137,36 +501,7 @@ impl DivInspector {
editor.set_show_runnables(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor
});
cx.subscribe_in(&style_editor, window, {
let id = id.clone();
move |this, editor, event: &EditorEvent, window, cx| match event {
EditorEvent::BufferEdited => {
let base_style_json = editor.read(cx).text(cx);
match serde_json_lenient::from_str(&base_style_json) {
Ok(new_base_style) => {
window.with_inspector_state::<DivInspectorState, _>(
Some(&id),
cx,
|state, _window| {
if let Some(state) = state.as_mut() {
*state.base_style = new_base_style;
}
},
);
window.refresh();
this.last_error = None;
}
Err(err) => this.last_error = Some(err.to_string().into()),
}
}
_ => {}
}
})
.detach();
self.style_editor = Some(style_editor);
}
}
@ -175,49 +510,223 @@ impl Render for DivInspector {
v_flex()
.size_full()
.gap_2()
.when_some(self.state.as_ref(), |this, state| {
.when_some(self.inspector_state.as_ref(), |this, inspector_state| {
this.child(
v_flex()
.child(Label::new("Layout").size(LabelSize::Large))
.child(render_layout_state(state, cx)),
.child(render_layout_state(inspector_state, cx)),
)
})
.when_some(self.style_editor.as_ref(), |this, style_editor| {
this.child(
v_flex()
.gap_2()
.child(Label::new("Style").size(LabelSize::Large))
.child(div().h_128().child(style_editor.clone()))
.when_some(self.last_error.as_ref(), |this, last_error| {
this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(last_error)),
.map(|this| match &self.state {
State::Loading | State::BuffersLoaded { .. } => {
this.child(Label::new("Loading..."))
}
State::LoadError { message } => this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(message)),
),
State::Ready {
rust_style_editor,
json_style_editor,
..
} => this
.child(
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Label::new("Rust Style").size(LabelSize::Large))
.child(
IconButton::new("reset-style", IconName::Eraser)
.tooltip(Tooltip::text("Reset style"))
.on_click(cx.listener(|this, _, _window, cx| {
this.reset_style(cx);
})),
),
)
}),
)
})
.when_none(&self.style_editor, |this| {
this.child(Label::new("Loading..."))
.child(div().h_64().child(rust_style_editor.clone())),
)
.child(
v_flex()
.gap_2()
.child(Label::new("JSON Style").size(LabelSize::Large))
.child(div().h_128().child(json_style_editor.clone()))
.when_some(self.json_style_error.as_ref(), |this, last_error| {
this.child(
div()
.w_full()
.border_1()
.border_color(Color::Error.color(cx))
.child(Label::new(last_error)),
)
}),
),
})
.into_any_element()
}
}
fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
v_flex()
.child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
.child(
div()
.text_ui(cx)
.child(format!("Bounds: {}", inspector_state.bounds)),
)
.child(
div()
.id("content-size")
.text_ui(cx)
.tooltip(Tooltip::text("Size of the element's children"))
.child(if state.content_size != state.bounds.size {
format!("Content size: {}", state.content_size)
} else {
"".to_string()
}),
.child(
if inspector_state.content_size != inspector_state.bounds.size {
format!("Content size: {}", inspector_state.content_size)
} else {
"".to_string()
},
),
)
}
static STYLE_METHODS: LazyLock<Vec<(Box<StyleRefinement>, FunctionReflection<StyleRefinement>)>> =
LazyLock::new(|| {
// Include StyledExt methods first so that those methods take precedence.
styled_ext_reflection::methods::<StyleRefinement>()
.into_iter()
.chain(styled_reflection::methods::<StyleRefinement>())
.map(|method| (Box::new(method.invoke(StyleRefinement::default())), method))
.collect()
});
fn guess_rust_code_from_style(goal_style: &StyleRefinement) -> (String, StyleRefinement) {
let mut subset_methods = Vec::new();
for (style, method) in STYLE_METHODS.iter() {
if goal_style.is_superset_of(style) {
subset_methods.push(method);
}
}
let mut code = "fn build() -> Div {\n div()".to_string();
let mut style = StyleRefinement::default();
for method in subset_methods {
let before_change = style.clone();
style = method.invoke(style);
if before_change != style {
let _ = write!(code, "\n .{}()", &method.name);
}
}
code.push_str("\n}");
(code, style)
}
fn is_not_identifier_char(c: char) -> bool {
!c.is_alphanumeric() && c != '_'
}
struct RustStyleCompletionProvider {
div_inspector: Entity<DivInspector>,
}
impl CompletionProvider for RustStyleCompletionProvider {
fn completions(
&self,
_excerpt_id: ExcerptId,
buffer: &Entity<Buffer>,
position: Anchor,
_: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<project::Completion>>>> {
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
else {
return Task::ready(Ok(Some(Vec::new())));
};
self.div_inspector.update(cx, |div_inspector, _cx| {
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
});
Task::ready(Ok(Some(
STYLE_METHODS
.iter()
.map(|(_, method)| Completion {
replace_range: replace_range.clone(),
new_text: format!(".{}()", method.name),
label: CodeLabel::plain(method.name.to_string(), None),
icon_path: None,
documentation: method.documentation.map(|documentation| {
CompletionDocumentation::MultiLineMarkdown(documentation.into())
}),
source: CompletionSource::Custom,
insert_text_mode: None,
confirm: None,
})
.collect(),
)))
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_: &str,
_: bool,
cx: &mut Context<Editor>,
) -> bool {
completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()
}
fn selection_changed(&self, mat: Option<&StringMatch>, _window: &mut Window, cx: &mut App) {
let div_inspector = self.div_inspector.clone();
let rust_completion = mat.as_ref().map(|mat| mat.string.clone());
cx.defer(move |cx| {
div_inspector.update(cx, |div_inspector, cx| {
div_inspector.handle_rust_completion_selection_change(rust_completion, cx);
});
});
}
fn sort_completions(&self) -> bool {
false
}
}
fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
let point = anchor.to_point(&snapshot);
let offset = point.to_offset(&snapshot);
let line_start = Point::new(point.row, 0).to_offset(&snapshot);
let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot);
let mut lines = snapshot.text_for_range(line_start..line_end).lines();
let line = lines.next()?;
let start_in_line = &line[..offset - line_start]
.rfind(|c| is_not_identifier_char(c) && c != '.')
.map(|ix| ix + 1)
.unwrap_or(0);
let end_in_line = &line[offset - line_start..]
.rfind(|c| is_not_identifier_char(c) && c != '(' && c != ')')
.unwrap_or(line_end - line_start);
if end_in_line > start_in_line {
let replace_start = snapshot.anchor_before(line_start + start_in_line);
let replace_end = snapshot.anchor_before(line_start + end_in_line);
Some(replace_start..replace_end)
} else {
None
}
}

View file

@ -24,7 +24,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
});
});
// Project used for editor buffers + LSP support
// Project used for editor buffers with LSP support
let project = project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
@ -57,14 +57,12 @@ fn render_inspector(
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()
@ -89,6 +87,8 @@ fn render_inspector(
)
.child(
v_flex()
.id("gpui-inspector-content")
.overflow_y_scroll()
.p_2()
.gap_2()
.when_some(inspector_id, |this, inspector_id| {
@ -101,26 +101,32 @@ fn render_inspector(
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
let source_location = inspector_id.path.source_location;
// For unknown reasons, for some elements the path is absolute.
let source_location_string = source_location.to_string();
let source_location_string = source_location_string
.strip_prefix(env!("ZED_REPO_DIR"))
.and_then(|s| s.strip_prefix("/"))
.map(|s| s.to_string())
.unwrap_or(source_location_string);
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("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))
.child(source_location_string)
.tooltip(Tooltip::text("Click to open by running zed cli"))
.on_click(move |_, _window, cx| {
cx.background_spawn(open_zed_source_location(source_location))
@ -131,7 +137,7 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
div()
.id("global-id")
.text_ui(cx)
.min_h_12()
.min_h_20()
.tooltip(Tooltip::text(
"GlobalElementId of the nearest ancestor with an ID",
))