logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
29
crates/inspector_ui/Cargo.toml
Normal file
29
crates/inspector_ui/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "inspector_ui"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/inspector_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
util_macros.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
1
crates/inspector_ui/LICENSE-GPL
Symbolic link
1
crates/inspector_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
112
crates/inspector_ui/README.md
Normal file
112
crates/inspector_ui/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 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.
|
||||
|
||||
# Current features
|
||||
|
||||
* Picking of elements via the mouse, with scroll wheel to inspect occluded elements.
|
||||
|
||||
* Temporary manipulation of the selected element.
|
||||
|
||||
* 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
|
||||
|
||||
## 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
|
||||
|
||||
* 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?
|
||||
|
||||
## Methods that take arguments in Rust style editor
|
||||
|
||||
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.
|
||||
|
||||
## Edit original code in Rust style editor
|
||||
|
||||
Two approaches:
|
||||
|
||||
1. Open an excerpt of the original file.
|
||||
|
||||
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.
|
||||
|
||||
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 component. 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:
|
||||
|
||||
* Support modifying multiple elements at once. This requires a way to specify which elements are modified - possibly wildcards in a match of the `InspectorElementId` path. This might default to ignoring all numeric parts and just matching on the names.
|
||||
|
||||
* Show a list of active modifications in the UI.
|
||||
|
||||
* Support for modifications being partial overrides instead of snapshots. A trickiness here is that multiple modifications may apply to the same element.
|
||||
|
||||
* 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
|
||||
|
||||
## Consider removing special side pane rendering
|
||||
|
||||
Currently the inspector has special rendering in the UI, but maybe it could just be a workspace item.
|
||||
|
||||
## Pull more inspector logic out of GPUI
|
||||
|
||||
Currently `crates/gpui/inspector.rs` and `crates/inspector_ui/inspector.rs` are quite entangled. It seems cleaner to pull as much logic a possible out of GPUI.
|
||||
|
||||
## Cleaner lifecycle for inspector state viewers / editors
|
||||
|
||||
Currently element state inspectors are just called on render. Ideally instead they would be implementors of some trait like:
|
||||
|
||||
```
|
||||
trait StateInspector: Render {
|
||||
fn new(cx: &mut App) -> Task<Self>;
|
||||
fn element_changed(inspector_id: &InspectorElementId, window: &mut Window, cx: &mut App);
|
||||
}
|
||||
```
|
||||
|
||||
See `div_inspector.rs` - it needs to initialize itself, keep track of its own loading state, and keep track of the last inspected ID in its render function.
|
||||
20
crates/inspector_ui/build.rs
Normal file
20
crates/inspector_ui/build.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
fn main() {
|
||||
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let mut path = std::path::PathBuf::from(&cargo_manifest_dir);
|
||||
|
||||
if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("inspector_ui") {
|
||||
panic!(
|
||||
"expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
|
||||
);
|
||||
}
|
||||
path.pop();
|
||||
|
||||
if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("crates") {
|
||||
panic!(
|
||||
"expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
|
||||
);
|
||||
}
|
||||
path.pop();
|
||||
|
||||
println!("cargo:rustc-env=ZED_REPO_DIR={}", path.display());
|
||||
}
|
||||
732
crates/inspector_ui/src/div_inspector.rs
Normal file
732
crates/inspector_ui/src/div_inspector.rs
Normal file
@@ -0,0 +1,732 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use editor::{
|
||||
Bias, CompletionProvider, Editor, EditorEvent, EditorMode, MinimapVisibility, MultiBuffer,
|
||||
};
|
||||
use fuzzy::StringMatch;
|
||||
use gpui::{
|
||||
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
|
||||
StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
|
||||
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
|
||||
};
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project,
|
||||
ProjectPath,
|
||||
};
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::LazyLock;
|
||||
use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
|
||||
use util::rel_path::RelPath;
|
||||
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_JSON: &str = util_macros::path!("/zed-inspector-style.json");
|
||||
|
||||
pub(crate) struct DivInspector {
|
||||
state: State,
|
||||
project: Entity<Project>,
|
||||
inspector_id: Option<InspectorElementId>,
|
||||
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 {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> DivInspector {
|
||||
// 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| {
|
||||
// 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.map(|rust_language| {
|
||||
cx.new(|cx| Buffer::local("", cx).with_language_async(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,
|
||||
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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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| {
|
||||
if 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| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
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) {
|
||||
if let State::Ready {
|
||||
rust_style_buffer,
|
||||
json_style_buffer,
|
||||
..
|
||||
} = &self.state
|
||||
{
|
||||
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<Entity<Buffer>> {
|
||||
let worktree = project
|
||||
.update(cx, |project, cx| project.create_worktree(path, false, cx))
|
||||
.await?;
|
||||
|
||||
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: RelPath::empty().into(),
|
||||
});
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_path(project_path, cx))
|
||||
.await?
|
||||
.1;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn create_editor(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Editor> {
|
||||
cx.new(|cx| {
|
||||
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(self.project.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_bookmarks(false, cx);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.disable_mouse_wheel_zoom();
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DivInspector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.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(inspector_state, cx)),
|
||||
)
|
||||
})
|
||||
.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);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.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(inspector_state: &DivInspectorState, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.child(
|
||||
div()
|
||||
.text_ui(cx)
|
||||
.child(format!(
|
||||
"Bounds: ⌜{} - {}⌟",
|
||||
inspector_state.bounds.origin,
|
||||
inspector_state.bounds.bottom_right()
|
||||
))
|
||||
.child(format!("Size: {}", inspector_state.bounds.size)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("content-size")
|
||||
.text_ui(cx)
|
||||
.tooltip(Tooltip::text("Size of the element's children"))
|
||||
.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,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
|
||||
else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
self.div_inspector.update(cx, |div_inspector, _cx| {
|
||||
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
|
||||
});
|
||||
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: STYLE_METHODS
|
||||
.iter()
|
||||
.map(|(_, method)| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}()", method.name),
|
||||
label: CodeLabel::plain(method.name.to_string(), None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: None,
|
||||
documentation: method.documentation.map(|documentation| {
|
||||
CompletionDocumentation::MultiLineMarkdown(documentation.into())
|
||||
}),
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
.collect(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<language::Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: 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_after(line_start + end_in_line);
|
||||
Some(replace_start..replace_end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
186
crates/inspector_ui/src/inspector.rs
Normal file
186
crates/inspector_ui/src/inspector.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, TaskExt, Window};
|
||||
use std::{cell::OnceCell, path::Path, sync::Arc};
|
||||
use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height};
|
||||
use util::{ResultExt as _, command::new_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 with 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,
|
||||
project::LocalProjectFlags {
|
||||
init_worktree_trust: false,
|
||||
..Default::default()
|
||||
},
|
||||
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_settings::setup_ui_font(window, cx);
|
||||
let colors = cx.theme().colors();
|
||||
let inspector_id = inspector.active_element_id();
|
||||
let toolbar_height = platform_title_bar_height(window);
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(colors.panel_background)
|
||||
.text_color(colors.text)
|
||||
.font(ui_font)
|
||||
.border_l_1()
|
||||
.border_color(colors.border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.pr_2()
|
||||
.pl_1()
|
||||
.mt_px()
|
||||
.h(toolbar_height)
|
||||
.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().justify_end().child(Label::new("GPUI Inspector"))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("gpui-inspector-content")
|
||||
.overflow_y_scroll()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.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;
|
||||
// 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(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Element ID").size(LabelSize::Large))
|
||||
.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()
|
||||
.font_buffer(cx)
|
||||
.text_xs()
|
||||
.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))
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("global-id")
|
||||
.text_ui(cx)
|
||||
.min_h_20()
|
||||
.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_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(())
|
||||
}
|
||||
}
|
||||
24
crates/inspector_ui/src/inspector_ui.rs
Normal file
24
crates/inspector_ui/src/inspector_ui.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[cfg(debug_assertions)]
|
||||
mod div_inspector;
|
||||
#[cfg(debug_assertions)]
|
||||
mod inspector;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub use inspector::init;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn init(_app_state: std::sync::Arc<workspace::AppState>, cx: &mut gpui::App) {
|
||||
use std::any::TypeId;
|
||||
use workspace::notifications::NotifyResultExt as _;
|
||||
|
||||
cx.on_action(|_: &zed_actions::dev::ToggleInspector, cx| {
|
||||
Err::<(), anyhow::Error>(anyhow::anyhow!(
|
||||
"dev::ToggleInspector is only available in debug builds"
|
||||
))
|
||||
.notify_app_err(cx);
|
||||
});
|
||||
|
||||
command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::dev::ToggleInspector>()]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user