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:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View 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.

View 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());
}

View 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
}
}

View 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(())
}
}

View 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>()]);
});
}