From 649072d14092bf0a54109ab536d5c8a71a22f40c Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 26 May 2025 11:43:57 -0600 Subject: [PATCH] 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. --- Cargo.lock | 3 + assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + crates/agent/src/agent_panel.rs | 5 +- .../src/context_picker/completion_provider.rs | 2 +- crates/agent/src/inline_prompt_editor.rs | 5 +- crates/agent/src/message_editor.rs | 3 +- .../src/context_editor.rs | 3 +- .../src/chat_panel/message_editor.rs | 2 +- .../src/session/running/console.rs | 2 +- crates/editor/src/code_context_menus.rs | 99 ++- crates/editor/src/editor.rs | 55 +- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/bounds_tree.rs | 17 +- crates/gpui/src/geometry.rs | 167 ++-- crates/gpui/src/inspector.rs | 31 + crates/gpui/src/scene.rs | 4 +- crates/gpui/src/style.rs | 8 +- crates/gpui/src/styled.rs | 4 + crates/gpui/src/taffy.rs | 12 +- crates/gpui/src/window.rs | 2 +- crates/gpui_macros/Cargo.toml | 6 +- .../src/derive_inspector_reflection.rs | 307 +++++++ crates/gpui_macros/src/gpui_macros.rs | 25 + .../tests/derive_inspector_reflection.rs | 148 ++++ crates/inspector_ui/Cargo.toml | 3 +- crates/inspector_ui/README.md | 60 +- crates/inspector_ui/src/div_inspector.rs | 775 +++++++++++++++--- crates/inspector_ui/src/inspector.rs | 38 +- .../src/derive_refineable.rs | 138 +++- crates/refineable/src/refineable.rs | 119 ++- crates/rules_library/src/rules_library.rs | 7 +- crates/ui/Cargo.toml | 1 + crates/ui/src/traits/styled_ext.rs | 1 + crates/util/src/util.rs | 38 + 35 files changed, 1778 insertions(+), 316 deletions(-) create mode 100644 crates/gpui_macros/src/derive_inspector_reflection.rs create mode 100644 crates/gpui_macros/tests/derive_inspector_reflection.rs diff --git a/Cargo.lock b/Cargo.lock index 21b0db58bc..efd9b5b824 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7120,6 +7120,7 @@ name = "gpui_macros" version = "0.1.0" dependencies = [ "gpui", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.101", @@ -8162,6 +8163,7 @@ dependencies = [ "anyhow", "command_palette_hooks", "editor", + "fuzzy", "gpui", "language", "project", @@ -16827,6 +16829,7 @@ dependencies = [ "component", "documented", "gpui", + "gpui_macros", "icons", "itertools 0.14.0", "menu", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 0e88b9e26f..23971bc458 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -676,6 +676,7 @@ { "bindings": { "ctrl-alt-shift-f": "workspace::FollowNextCollaborator", + // Only available in debug builds: opens an element inspector for development. "ctrl-alt-i": "dev::ToggleInspector" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 0193657434..b8ea238f68 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -736,6 +736,7 @@ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", + // Only available in debug builds: opens an element inspector for development. "cmd-alt-i": "dev::ToggleInspector" } }, diff --git a/crates/agent/src/agent_panel.rs b/crates/agent/src/agent_panel.rs index 26e8b10dc4..a7cbfba5c7 100644 --- a/crates/agent/src/agent_panel.rs +++ b/crates/agent/src/agent_panel.rs @@ -1,5 +1,6 @@ use std::ops::Range; use std::path::Path; +use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -915,8 +916,8 @@ impl AgentPanel { open_rules_library( self.language_registry.clone(), Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())), - Arc::new(|| { - Box::new(SlashCommandCompletionProvider::new( + Rc::new(|| { + Rc::new(SlashCommandCompletionProvider::new( Arc::new(SlashCommandWorkingSet::default()), None, None, diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index 1c6acaa849..7d760dd295 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -1289,7 +1289,7 @@ mod tests { .map(Entity::downgrade) }); window.focus(&editor.focus_handle(cx)); - editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( workspace.downgrade(), context_store.downgrade(), None, diff --git a/crates/agent/src/inline_prompt_editor.rs b/crates/agent/src/inline_prompt_editor.rs index c086541f2d..08c8060bfa 100644 --- a/crates/agent/src/inline_prompt_editor.rs +++ b/crates/agent/src/inline_prompt_editor.rs @@ -28,6 +28,7 @@ use language_model::{LanguageModel, LanguageModelRegistry}; use parking_lot::Mutex; use settings::Settings; use std::cmp; +use std::rc::Rc; use std::sync::Arc; use theme::ThemeSettings; use ui::utils::WithRemSize; @@ -890,7 +891,7 @@ impl PromptEditor { let prompt_editor_entity = prompt_editor.downgrade(); prompt_editor.update(cx, |editor, _| { - editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( workspace.clone(), context_store.downgrade(), thread_store.clone(), @@ -1061,7 +1062,7 @@ impl PromptEditor { let prompt_editor_entity = prompt_editor.downgrade(); prompt_editor.update(cx, |editor, _| { - editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( workspace.clone(), context_store.downgrade(), thread_store.clone(), diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 6d2d17b20c..2588899713 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::rc::Rc; use std::sync::Arc; use crate::agent_model_selector::{AgentModelSelector, ModelType}; @@ -121,7 +122,7 @@ pub(crate) fn create_editor( let editor_entity = editor.downgrade(); editor.update(cx, |editor, _| { - editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( workspace, context_store, Some(thread_store), diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index cd2134b786..4c00e4414e 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -51,6 +51,7 @@ use std::{ cmp, ops::Range, path::{Path, PathBuf}, + rc::Rc, sync::Arc, time::Duration, }; @@ -234,7 +235,7 @@ impl ContextEditor { editor.set_show_breakpoints(false, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); - editor.set_completion_provider(Some(Box::new(completion_provider))); + editor.set_completion_provider(Some(Rc::new(completion_provider))); editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never); editor.set_collaboration_hub(Box::new(project.clone())); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index d9cb0ade33..46d3b36bd4 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -112,7 +112,7 @@ impl MessageEditor { editor.set_show_gutter(false, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); - editor.set_completion_provider(Some(Box::new(MessageEditorCompletionProvider(this)))); + editor.set_completion_provider(Some(Rc::new(MessageEditorCompletionProvider(this)))); editor.set_auto_replace_emoji_shortcode( MessageEditorSettings::get_global(cx) .auto_replace_emoji_shortcode diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 73a399d78c..bff7793ee4 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -72,7 +72,7 @@ impl Console { editor.set_show_gutter(false, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); - editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this)))); + editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this)))); editor }); diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 57cbd3c24e..c28f788ec8 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1,9 +1,9 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior, - ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, UniformListScrollHandle, - div, px, uniform_list, + AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString, + Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list, }; +use gpui::{AsyncWindowContext, WeakEntity}; use language::Buffer; use language::CodeLabel; use markdown::{Markdown, MarkdownElement}; @@ -50,11 +50,12 @@ impl CodeContextMenu { pub fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) -> bool { if self.visible() { match self { - CodeContextMenu::Completions(menu) => menu.select_first(provider, cx), + CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx), CodeContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -66,11 +67,12 @@ impl CodeContextMenu { pub fn select_prev( &mut self, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) -> bool { if self.visible() { match self { - CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx), + CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx), CodeContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -82,11 +84,12 @@ impl CodeContextMenu { pub fn select_next( &mut self, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) -> bool { if self.visible() { match self { - CodeContextMenu::Completions(menu) => menu.select_next(provider, cx), + CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx), CodeContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -98,11 +101,12 @@ impl CodeContextMenu { pub fn select_last( &mut self, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) -> bool { if self.visible() { match self { - CodeContextMenu::Completions(menu) => menu.select_last(provider, cx), + CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx), CodeContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -290,6 +294,7 @@ impl CompletionsMenu { fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) { let index = if self.scroll_handle.y_flipped() { @@ -297,40 +302,56 @@ impl CompletionsMenu { } else { 0 }; - self.update_selection_index(index, provider, cx); + self.update_selection_index(index, provider, window, cx); } - fn select_last(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + fn select_last( + &mut self, + provider: Option<&dyn CompletionProvider>, + window: &mut Window, + cx: &mut Context, + ) { let index = if self.scroll_handle.y_flipped() { 0 } else { self.entries.borrow().len() - 1 }; - self.update_selection_index(index, provider, cx); + self.update_selection_index(index, provider, window, cx); } - fn select_prev(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + fn select_prev( + &mut self, + provider: Option<&dyn CompletionProvider>, + window: &mut Window, + cx: &mut Context, + ) { let index = if self.scroll_handle.y_flipped() { self.next_match_index() } else { self.prev_match_index() }; - self.update_selection_index(index, provider, cx); + self.update_selection_index(index, provider, window, cx); } - fn select_next(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + fn select_next( + &mut self, + provider: Option<&dyn CompletionProvider>, + window: &mut Window, + cx: &mut Context, + ) { let index = if self.scroll_handle.y_flipped() { self.prev_match_index() } else { self.next_match_index() }; - self.update_selection_index(index, provider, cx); + self.update_selection_index(index, provider, window, cx); } fn update_selection_index( &mut self, match_index: usize, provider: Option<&dyn CompletionProvider>, + window: &mut Window, cx: &mut Context, ) { if self.selected_item != match_index { @@ -338,6 +359,9 @@ impl CompletionsMenu { self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); self.resolve_visible_completions(provider, cx); + if let Some(provider) = provider { + self.handle_selection_changed(provider, window, cx); + } cx.notify(); } } @@ -358,6 +382,21 @@ impl CompletionsMenu { } } + fn handle_selection_changed( + &self, + provider: &dyn CompletionProvider, + window: &mut Window, + cx: &mut App, + ) { + let entries = self.entries.borrow(); + let entry = if self.selected_item < entries.len() { + Some(&entries[self.selected_item]) + } else { + None + }; + provider.selection_changed(entry, window, cx); + } + pub fn resolve_visible_completions( &mut self, provider: Option<&dyn CompletionProvider>, @@ -753,7 +792,13 @@ impl CompletionsMenu { }); } - pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { + pub async fn filter( + &mut self, + query: Option<&str>, + provider: Option>, + editor: WeakEntity, + cx: &mut AsyncWindowContext, + ) { let mut matches = if let Some(query) = query { fuzzy::match_strings( &self.match_candidates, @@ -761,7 +806,7 @@ impl CompletionsMenu { query.chars().any(|c| c.is_uppercase()), 100, &Default::default(), - executor, + cx.background_executor().clone(), ) .await } else { @@ -822,6 +867,28 @@ impl CompletionsMenu { self.selected_item = 0; // This keeps the display consistent when y_flipped. self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); + + if let Some(provider) = provider { + cx.update(|window, cx| { + // Since this is async, it's possible the menu has been closed and possibly even + // another opened. `provider.selection_changed` should not be called in this case. + let this_menu_still_active = editor + .read_with(cx, |editor, _cx| { + if let Some(CodeContextMenu::Completions(completions_menu)) = + editor.context_menu.borrow().as_ref() + { + completions_menu.id == self.id + } else { + false + } + }) + .unwrap_or(false); + if this_menu_still_active { + self.handle_selection_changed(&*provider, window, cx); + } + }) + .ok(); + } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ef6e743942..813801d9bc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -77,7 +77,7 @@ use futures::{ FutureExt, future::{self, Shared, join}, }; -use fuzzy::StringMatchCandidate; +use fuzzy::{StringMatch, StringMatchCandidate}; use ::git::blame::BlameEntry; use ::git::{Restore, blame::ParsedCommitMessage}; @@ -912,7 +912,7 @@ pub struct Editor { // TODO: make this a access method pub project: Option>, semantics_provider: Option>, - completion_provider: Option>, + completion_provider: Option>, collaboration_hub: Option>, blink_manager: Entity, show_cursor_names: bool, @@ -1755,7 +1755,7 @@ impl Editor { soft_wrap_mode_override, diagnostics_max_severity, hard_wrap: None, - completion_provider: project.clone().map(|project| Box::new(project) as _), + completion_provider: project.clone().map(|project| Rc::new(project) as _), semantics_provider: project.clone().map(|project| Rc::new(project) as _), collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, @@ -2374,7 +2374,7 @@ impl Editor { self.custom_context_menu = Some(Box::new(f)) } - pub fn set_completion_provider(&mut self, provider: Option>) { + pub fn set_completion_provider(&mut self, provider: Option>) { self.completion_provider = provider; } @@ -2684,9 +2684,10 @@ impl Editor { drop(context_menu); let query = Self::completion_query(buffer, cursor_position); - cx.spawn(async move |this, cx| { + let completion_provider = self.completion_provider.clone(); + cx.spawn_in(window, async move |this, cx| { completion_menu - .filter(query.as_deref(), cx.background_executor().clone()) + .filter(query.as_deref(), completion_provider, this.clone(), cx) .await; this.update(cx, |this, cx| { @@ -4960,15 +4961,16 @@ impl Editor { let word_search_range = buffer_snapshot.point_to_offset(min_word_search) ..buffer_snapshot.point_to_offset(max_word_search); - let provider = self - .completion_provider - .as_ref() - .filter(|_| !ignore_completion_provider); + let provider = if ignore_completion_provider { + None + } else { + self.completion_provider.clone() + }; let skip_digits = query .as_ref() .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); - let (mut words, provided_completions) = match provider { + let (mut words, provided_completions) = match &provider { Some(provider) => { let completions = provider.completions( position.excerpt_id, @@ -5071,7 +5073,9 @@ impl Editor { } else { None }, - cx.background_executor().clone(), + provider, + editor.clone(), + cx, ) .await; @@ -8651,6 +8655,11 @@ impl Editor { let context_menu = self.context_menu.borrow_mut().take(); self.stale_inline_completion_in_menu.take(); self.update_visible_inline_completion(window, cx); + if let Some(CodeContextMenu::Completions(_)) = &context_menu { + if let Some(completion_provider) = &self.completion_provider { + completion_provider.selection_changed(None, window, cx); + } + } context_menu } @@ -11353,7 +11362,7 @@ impl Editor { .context_menu .borrow_mut() .as_mut() - .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) + .map(|menu| menu.select_first(self.completion_provider.as_deref(), window, cx)) .unwrap_or(false) { return; @@ -11477,7 +11486,7 @@ impl Editor { .context_menu .borrow_mut() .as_mut() - .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) + .map(|menu| menu.select_last(self.completion_provider.as_deref(), window, cx)) .unwrap_or(false) { return; @@ -11532,44 +11541,44 @@ impl Editor { pub fn context_menu_first( &mut self, _: &ContextMenuFirst, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_first(self.completion_provider.as_deref(), cx); + context_menu.select_first(self.completion_provider.as_deref(), window, cx); } } pub fn context_menu_prev( &mut self, _: &ContextMenuPrevious, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_prev(self.completion_provider.as_deref(), cx); + context_menu.select_prev(self.completion_provider.as_deref(), window, cx); } } pub fn context_menu_next( &mut self, _: &ContextMenuNext, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_next(self.completion_provider.as_deref(), cx); + context_menu.select_next(self.completion_provider.as_deref(), window, cx); } } pub fn context_menu_last( &mut self, _: &ContextMenuLast, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_last(self.completion_provider.as_deref(), cx); + context_menu.select_last(self.completion_provider.as_deref(), window, cx); } } @@ -19615,6 +19624,8 @@ pub trait CompletionProvider { cx: &mut Context, ) -> bool; + fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {} + fn sort_completions(&self) -> bool { true } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 522b773bca..0a2903f643 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -22,7 +22,7 @@ test-support = [ "wayland", "x11", ] -inspector = [] +inspector = ["gpui_macros/inspector"] leak-detection = ["backtrace"] runtime_shaders = [] macos-blade = [ diff --git a/crates/gpui/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs index c2a6b2b943..44840ac1d3 100644 --- a/crates/gpui/src/bounds_tree.rs +++ b/crates/gpui/src/bounds_tree.rs @@ -8,7 +8,7 @@ use std::{ #[derive(Debug)] pub(crate) struct BoundsTree where - U: Default + Clone + Debug, + U: Clone + Debug + Default + PartialEq, { root: Option, nodes: Vec>, @@ -17,7 +17,14 @@ where impl BoundsTree where - U: Clone + Debug + PartialOrd + Add + Sub + Half + Default, + U: Clone + + Debug + + PartialEq + + PartialOrd + + Add + + Sub + + Half + + Default, { pub fn clear(&mut self) { self.root = None; @@ -174,7 +181,7 @@ where impl Default for BoundsTree where - U: Default + Clone + Debug, + U: Clone + Debug + Default + PartialEq, { fn default() -> Self { BoundsTree { @@ -188,7 +195,7 @@ where #[derive(Debug, Clone)] enum Node where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { Leaf { bounds: Bounds, @@ -204,7 +211,7 @@ where impl Node where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { fn bounds(&self) -> &Bounds { match self { diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 5f0763e12b..a0b46567c2 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -76,9 +76,9 @@ pub trait Along { JsonSchema, Hash, )] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[repr(C)] -pub struct Point { +pub struct Point { /// The x coordinate of the point. pub x: T, /// The y coordinate of the point. @@ -104,11 +104,11 @@ pub struct Point { /// assert_eq!(p.x, 10); /// assert_eq!(p.y, 20); /// ``` -pub const fn point(x: T, y: T) -> Point { +pub const fn point(x: T, y: T) -> Point { Point { x, y } } -impl Point { +impl Point { /// Creates a new `Point` with the specified `x` and `y` coordinates. /// /// # Arguments @@ -145,7 +145,7 @@ impl Point { /// let p_float = p.map(|coord| coord as f32); /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); /// ``` - pub fn map(&self, f: impl Fn(T) -> U) -> Point { + pub fn map(&self, f: impl Fn(T) -> U) -> Point { Point { x: f(self.x.clone()), y: f(self.y.clone()), @@ -153,7 +153,7 @@ impl Point { } } -impl Along for Point { +impl Along for Point { type Unit = T; fn along(&self, axis: Axis) -> T { @@ -177,7 +177,7 @@ impl Along for Point { } } -impl Negate for Point { +impl Negate for Point { fn negate(self) -> Self { self.map(Negate::negate) } @@ -222,7 +222,7 @@ impl Point { impl Point where - T: Sub + Debug + Clone + Default, + T: Sub + Clone + Debug + Default + PartialEq, { /// Get the position of this point, relative to the given origin pub fn relative_to(&self, origin: &Point) -> Point { @@ -235,7 +235,7 @@ where impl Mul for Point where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, Rhs: Clone + Debug, { type Output = Point; @@ -250,7 +250,7 @@ where impl MulAssign for Point where - T: Clone + Mul + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, S: Clone, { fn mul_assign(&mut self, rhs: S) { @@ -261,7 +261,7 @@ where impl Div for Point where - T: Div + Clone + Default + Debug, + T: Div + Clone + Debug + Default + PartialEq, S: Clone, { type Output = Self; @@ -276,7 +276,7 @@ where impl Point where - T: PartialOrd + Clone + Default + Debug, + T: PartialOrd + Clone + Debug + Default + PartialEq, { /// Returns a new point with the maximum values of each dimension from `self` and `other`. /// @@ -369,7 +369,7 @@ where } } -impl Clone for Point { +impl Clone for Point { fn clone(&self) -> Self { Self { x: self.x.clone(), @@ -378,7 +378,7 @@ impl Clone for Point { } } -impl Display for Point { +impl Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } @@ -389,16 +389,16 @@ impl Display for Point { /// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. /// It is commonly used to specify dimensions for elements in a UI, such as a window or element. #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[repr(C)] -pub struct Size { +pub struct Size { /// The width component of the size. pub width: T, /// The height component of the size. pub height: T, } -impl Size { +impl Size { /// Create a new Size, a synonym for [`size`] pub fn new(width: T, height: T) -> Self { size(width, height) @@ -422,14 +422,14 @@ impl Size { /// ``` pub const fn size(width: T, height: T) -> Size where - T: Clone + Default + Debug, + T: Clone + Debug + Default + PartialEq, { Size { width, height } } impl Size where - T: Clone + Default + Debug, + T: Clone + Debug + Default + PartialEq, { /// Applies a function to the width and height of the size, producing a new `Size`. /// @@ -451,7 +451,7 @@ where /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Size where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { Size { width: f(self.width.clone()), @@ -462,7 +462,7 @@ where impl Size where - T: Clone + Default + Debug + Half, + T: Clone + Debug + Default + PartialEq + Half, { /// Compute the center point of the size.g pub fn center(&self) -> Point { @@ -502,7 +502,7 @@ impl Size { impl Along for Size where - T: Clone + Default + Debug, + T: Clone + Debug + Default + PartialEq, { type Unit = T; @@ -530,7 +530,7 @@ where impl Size where - T: PartialOrd + Clone + Default + Debug, + T: PartialOrd + Clone + Debug + Default + PartialEq, { /// Returns a new `Size` with the maximum width and height from `self` and `other`. /// @@ -595,7 +595,7 @@ where impl Sub for Size where - T: Sub + Clone + Default + Debug, + T: Sub + Clone + Debug + Default + PartialEq, { type Output = Size; @@ -609,7 +609,7 @@ where impl Add for Size where - T: Add + Clone + Default + Debug, + T: Add + Clone + Debug + Default + PartialEq, { type Output = Size; @@ -623,8 +623,8 @@ where impl Mul for Size where - T: Mul + Clone + Default + Debug, - Rhs: Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug + Default + PartialEq, { type Output = Size; @@ -638,7 +638,7 @@ where impl MulAssign for Size where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, S: Clone, { fn mul_assign(&mut self, rhs: S) { @@ -647,24 +647,24 @@ where } } -impl Eq for Size where T: Eq + Default + Debug + Clone {} +impl Eq for Size where T: Eq + Clone + Debug + Default + PartialEq {} impl Debug for Size where - T: Clone + Default + Debug, + T: Clone + Debug + Default + PartialEq, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) } } -impl Display for Size { +impl Display for Size { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} × {}", self.width, self.height) } } -impl From> for Size { +impl From> for Size { fn from(point: Point) -> Self { Self { width: point.x, @@ -746,7 +746,7 @@ impl Size { #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] #[refineable(Debug)] #[repr(C)] -pub struct Bounds { +pub struct Bounds { /// The origin point of this area. pub origin: Point, /// The size of the rectangle. @@ -754,7 +754,10 @@ pub struct Bounds { } /// Create a bounds with the given origin and size -pub fn bounds(origin: Point, size: Size) -> Bounds { +pub fn bounds( + origin: Point, + size: Size, +) -> Bounds { Bounds { origin, size } } @@ -790,7 +793,7 @@ impl Bounds { impl Bounds where - T: Clone + Debug + Default, + T: Clone + Debug + Default + PartialEq, { /// Creates a new `Bounds` with the specified origin and size. /// @@ -809,7 +812,7 @@ where impl Bounds where - T: Clone + Debug + Sub + Default, + T: Sub + Clone + Debug + Default + PartialEq, { /// Constructs a `Bounds` from two corner points: the top left and bottom right corners. /// @@ -875,7 +878,7 @@ where impl Bounds where - T: Clone + Debug + Sub + Default + Half, + T: Sub + Half + Clone + Debug + Default + PartialEq, { /// Creates a new bounds centered at the given point. pub fn centered_at(center: Point, size: Size) -> Self { @@ -889,7 +892,7 @@ where impl Bounds where - T: Clone + Debug + PartialOrd + Add + Default, + T: PartialOrd + Add + Clone + Debug + Default + PartialEq, { /// Checks if this `Bounds` intersects with another `Bounds`. /// @@ -937,7 +940,7 @@ where impl Bounds where - T: Clone + Debug + Add + Default + Half, + T: Add + Half + Clone + Debug + Default + PartialEq, { /// Returns the center point of the bounds. /// @@ -970,7 +973,7 @@ where impl Bounds where - T: Clone + Debug + Add + Default, + T: Add + Clone + Debug + Default + PartialEq, { /// Calculates the half perimeter of a rectangle defined by the bounds. /// @@ -997,7 +1000,7 @@ where impl Bounds where - T: Clone + Debug + Add + Sub + Default, + T: Add + Sub + Clone + Debug + Default + PartialEq, { /// Dilates the bounds by a specified amount in all directions. /// @@ -1048,7 +1051,13 @@ where impl Bounds where - T: Clone + Debug + Add + Sub + Neg + Default, + T: Add + + Sub + + Neg + + Clone + + Debug + + Default + + PartialEq, { /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. /// @@ -1058,7 +1067,9 @@ where } } -impl + Sub> Bounds { +impl + Sub + Clone + Debug + Default + PartialEq> + Bounds +{ /// Calculates the intersection of two `Bounds` objects. /// /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, @@ -1140,7 +1151,7 @@ impl + Sub Bounds where - T: Clone + Debug + Add + Sub + Default, + T: Add + Sub + Clone + Debug + Default + PartialEq, { /// Computes the space available within outer bounds. pub fn space_within(&self, outer: &Self) -> Edges { @@ -1155,9 +1166,9 @@ where impl Mul for Bounds where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, Point: Mul>, - Rhs: Clone + Default + Debug, + Rhs: Clone + Debug + Default + PartialEq, { type Output = Bounds; @@ -1171,7 +1182,7 @@ where impl MulAssign for Bounds where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, S: Clone, { fn mul_assign(&mut self, rhs: S) { @@ -1183,7 +1194,7 @@ where impl Div for Bounds where Size: Div>, - T: Div + Default + Clone + Debug, + T: Div + Clone + Debug + Default + PartialEq, S: Clone, { type Output = Self; @@ -1198,7 +1209,7 @@ where impl Add> for Bounds where - T: Add + Default + Clone + Debug, + T: Add + Clone + Debug + Default + PartialEq, { type Output = Self; @@ -1212,7 +1223,7 @@ where impl Sub> for Bounds where - T: Sub + Default + Clone + Debug, + T: Sub + Clone + Debug + Default + PartialEq, { type Output = Self; @@ -1226,7 +1237,7 @@ where impl Bounds where - T: Add + Clone + Default + Debug, + T: Add + Clone + Debug + Default + PartialEq, { /// Returns the top edge of the bounds. /// @@ -1365,7 +1376,7 @@ where impl Bounds where - T: Add + PartialOrd + Clone + Default + Debug, + T: Add + PartialOrd + Clone + Debug + Default + PartialEq, { /// Checks if the given point is within the bounds. /// @@ -1472,7 +1483,7 @@ where /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Bounds where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { Bounds { origin: self.origin.map(&f), @@ -1531,7 +1542,7 @@ where impl Bounds where - T: Add + PartialOrd + Clone + Default + Debug + Sub, + T: Add + Sub + PartialOrd + Clone + Debug + Default + PartialEq, { /// Convert a point to the coordinate space defined by this Bounds pub fn localize(&self, point: &Point) -> Option> { @@ -1545,7 +1556,7 @@ where /// # Returns /// /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. -impl Bounds { +impl Bounds { /// Checks if the bounds represent an empty area. /// /// # Returns @@ -1556,7 +1567,7 @@ impl Bounds { } } -impl> Display for Bounds { +impl> Display for Bounds { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -1651,7 +1662,7 @@ impl Bounds { } } -impl Copy for Bounds {} +impl Copy for Bounds {} /// Represents the edges of a box in a 2D space, such as padding or margin. /// @@ -1674,9 +1685,9 @@ impl Copy for Bounds {} /// assert_eq!(edges.left, 40.0); /// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[repr(C)] -pub struct Edges { +pub struct Edges { /// The size of the top edge. pub top: T, /// The size of the right edge. @@ -1689,7 +1700,7 @@ pub struct Edges { impl Mul for Edges where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, { type Output = Self; @@ -1705,7 +1716,7 @@ where impl MulAssign for Edges where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, S: Clone, { fn mul_assign(&mut self, rhs: S) { @@ -1716,9 +1727,9 @@ where } } -impl Copy for Edges {} +impl Copy for Edges {} -impl Edges { +impl Edges { /// Constructs `Edges` where all sides are set to the same specified value. /// /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized @@ -1776,7 +1787,7 @@ impl Edges { /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Edges where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { Edges { top: f(&self.top), @@ -2151,9 +2162,9 @@ impl Corner { /// /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[repr(C)] -pub struct Corners { +pub struct Corners { /// The value associated with the top left corner. pub top_left: T, /// The value associated with the top right corner. @@ -2166,7 +2177,7 @@ pub struct Corners { impl Corners where - T: Clone + Default + Debug, + T: Clone + Debug + Default + PartialEq, { /// Constructs `Corners` where all sides are set to the same specified value. /// @@ -2319,7 +2330,7 @@ impl Corners { } } -impl + Ord + Clone + Default + Debug> Corners { +impl + Ord + Clone + Debug + Default + PartialEq> Corners { /// Clamps corner radii to be less than or equal to half the shortest side of a quad. /// /// # Arguments @@ -2340,7 +2351,7 @@ impl + Ord + Clone + Default + Debug> Corners { } } -impl Corners { +impl Corners { /// Applies a function to each field of the `Corners`, producing a new `Corners`. /// /// This method allows for converting a `Corners` to a `Corners` by specifying a closure @@ -2375,7 +2386,7 @@ impl Corners { /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Corners where - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { Corners { top_left: f(&self.top_left), @@ -2388,7 +2399,7 @@ impl Corners { impl Mul for Corners where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, { type Output = Self; @@ -2404,7 +2415,7 @@ where impl MulAssign for Corners where - T: Mul + Clone + Default + Debug, + T: Mul + Clone + Debug + Default + PartialEq, S: Clone, { fn mul_assign(&mut self, rhs: S) { @@ -2415,7 +2426,7 @@ where } } -impl Copy for Corners where T: Copy + Clone + Default + Debug {} +impl Copy for Corners where T: Copy + Clone + Debug + Default + PartialEq {} impl From for Corners { fn from(val: f32) -> Self { @@ -3427,7 +3438,7 @@ impl Default for DefiniteLength { } /// A length that can be defined in pixels, rems, percent of parent, or auto. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum Length { /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. Definite(DefiniteLength), @@ -3772,7 +3783,7 @@ impl IsZero for Length { } } -impl IsZero for Point { +impl IsZero for Point { fn is_zero(&self) -> bool { self.x.is_zero() && self.y.is_zero() } @@ -3780,14 +3791,14 @@ impl IsZero for Point { impl IsZero for Size where - T: IsZero + Default + Debug + Clone, + T: IsZero + Clone + Debug + Default + PartialEq, { fn is_zero(&self) -> bool { self.width.is_zero() || self.height.is_zero() } } -impl IsZero for Bounds { +impl IsZero for Bounds { fn is_zero(&self) -> bool { self.size.is_zero() } @@ -3795,7 +3806,7 @@ impl IsZero for Bounds { impl IsZero for Corners where - T: IsZero + Clone + Default + Debug, + T: IsZero + Clone + Debug + Default + PartialEq, { fn is_zero(&self) -> bool { self.top_left.is_zero() diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index 7b50ed54d1..23c46edcc1 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -221,3 +221,34 @@ mod conditional { } } } + +/// Provides definitions used by `#[derive_inspector_reflection]`. +#[cfg(any(feature = "inspector", debug_assertions))] +pub mod inspector_reflection { + use std::any::Any; + + /// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name, + /// documentation, and ability to invoke the function. + #[derive(Clone, Copy)] + pub struct FunctionReflection { + /// The name of the function + pub name: &'static str, + /// The method + pub function: fn(Box) -> Box, + /// Documentation for the function + pub documentation: Option<&'static str>, + /// `PhantomData` for the type of the argument and result + pub _type: std::marker::PhantomData, + } + + impl FunctionReflection { + /// Invoke this method on a value and return the result. + pub fn invoke(&self, value: T) -> T { + let boxed = Box::new(value) as Box; + let result = (self.function)(boxed); + *result + .downcast::() + .expect("Type mismatch in reflection invoke") + } + } +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 51406ea6dd..806054cefc 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -679,7 +679,7 @@ pub(crate) struct PathId(pub(crate) usize); /// A line made up of a series of vertices and control points. #[derive(Clone, Debug)] -pub struct Path { +pub struct Path { pub(crate) id: PathId, order: DrawOrder, pub(crate) bounds: Bounds

, @@ -812,7 +812,7 @@ impl From> for Primitive { #[derive(Clone, Debug)] #[repr(C)] -pub(crate) struct PathVertex { +pub(crate) struct PathVertex { pub(crate) xy_position: Point

, pub(crate) st_position: Point, pub(crate) content_mask: ContentMask

, diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 91d148047e..560de7b924 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -140,7 +140,7 @@ impl ObjectFit { /// The CSS styling that can be applied to an element via the `Styled` trait #[derive(Clone, Refineable, Debug)] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Style { /// What layout strategy should be used? pub display: Display, @@ -286,7 +286,7 @@ pub enum Visibility { } /// The possible values of the box-shadow property -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct BoxShadow { /// What color should the shadow have? pub color: Hsla, @@ -332,7 +332,7 @@ pub enum TextAlign { /// The properties that can be used to style text in GPUI #[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, Serialize, Deserialize, JsonSchema)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct TextStyle { /// The color of the text pub color: Hsla, @@ -794,7 +794,7 @@ pub struct StrikethroughStyle { } /// The kinds of fill that can be applied to a shape. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum Fill { /// A solid color fill. Color(Background), diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index c91cfabce0..b689f32687 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -14,6 +14,10 @@ const ELLIPSIS: SharedString = SharedString::new_static("…"); /// A trait for elements that can be styled. /// Use this to opt-in to a utility CSS-like styling API. +#[cfg_attr( + any(feature = "inspector", debug_assertions), + gpui_macros::derive_inspector_reflection +)] pub trait Styled: Sized { /// Returns a reference to the style memory of this element. fn style(&mut self) -> &mut StyleRefinement; diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 094f8281f3..597bff13e2 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -359,7 +359,7 @@ impl ToTaffy for AbsoluteLength { impl From> for Point where T: Into, - T2: Clone + Default + Debug, + T2: Clone + Debug + Default + PartialEq, { fn from(point: TaffyPoint) -> Point { Point { @@ -371,7 +371,7 @@ where impl From> for TaffyPoint where - T: Into + Clone + Default + Debug, + T: Into + Clone + Debug + Default + PartialEq, { fn from(val: Point) -> Self { TaffyPoint { @@ -383,7 +383,7 @@ where impl ToTaffy> for Size where - T: ToTaffy + Clone + Default + Debug, + T: ToTaffy + Clone + Debug + Default + PartialEq, { fn to_taffy(&self, rem_size: Pixels) -> TaffySize { TaffySize { @@ -395,7 +395,7 @@ where impl ToTaffy> for Edges where - T: ToTaffy + Clone + Default + Debug, + T: ToTaffy + Clone + Debug + Default + PartialEq, { fn to_taffy(&self, rem_size: Pixels) -> TaffyRect { TaffyRect { @@ -410,7 +410,7 @@ where impl From> for Size where T: Into, - U: Clone + Default + Debug, + U: Clone + Debug + Default + PartialEq, { fn from(taffy_size: TaffySize) -> Self { Size { @@ -422,7 +422,7 @@ where impl From> for TaffySize where - T: Into + Clone + Default + Debug, + T: Into + Clone + Debug + Default + PartialEq, { fn from(size: Size) -> Self { TaffySize { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index d3c50a5cd7..f78bcad3ec 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -979,7 +979,7 @@ pub(crate) struct DispatchEventResult { /// to leave room to support more complex shapes in the future. #[derive(Clone, Debug, Default, PartialEq, Eq)] #[repr(C)] -pub struct ContentMask { +pub struct ContentMask { /// The bounds pub bounds: Bounds

, } diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 65b5eaf955..6dad698177 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -8,16 +8,20 @@ license = "Apache-2.0" [lints] workspace = true +[features] +inspector = [] + [lib] path = "src/gpui_macros.rs" proc-macro = true doctest = true [dependencies] +heck.workspace = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true workspace-hack.workspace = true [dev-dependencies] -gpui.workspace = true +gpui = { workspace = true, features = ["inspector"] } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs new file mode 100644 index 0000000000..fa22f95f9a --- /dev/null +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -0,0 +1,307 @@ +//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods +//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4. + +use heck::ToSnakeCase as _; +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{ + Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type, + parse_macro_input, parse_quote, + visit_mut::{self, VisitMut}, +}; + +pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut item = parse_macro_input!(input as Item); + + // First, expand any macros in the trait + match &mut item { + Item::Trait(trait_item) => { + let mut expander = MacroExpander; + expander.visit_item_trait_mut(trait_item); + } + _ => { + return syn::Error::new_spanned( + quote!(#item), + "#[derive_inspector_reflection] can only be applied to traits", + ) + .to_compile_error() + .into(); + } + } + + // Now process the expanded trait + match item { + Item::Trait(trait_item) => generate_reflected_trait(trait_item), + _ => unreachable!(), + } +} + +fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream { + let trait_name = &trait_item.ident; + let vis = &trait_item.vis; + + // Determine if we're being called from within the gpui crate + let call_site = Span::call_site(); + let inspector_reflection_path = if is_called_from_gpui_crate(call_site) { + quote! { crate::inspector_reflection } + } else { + quote! { ::gpui::inspector_reflection } + }; + + // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self + let mut method_infos = Vec::new(); + + for item in &trait_item.items { + if let TraitItem::Fn(method) = item { + let method_name = &method.sig.ident; + + // Check if method has self or mut self receiver + let has_valid_self_receiver = method + .sig + .inputs + .iter() + .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none())); + + // Check if method returns Self + let returns_self = match &method.sig.output { + ReturnType::Type(_, ty) => { + matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self")) + } + ReturnType::Default => false, + }; + + // Check if method has exactly one parameter (self or mut self) + let param_count = method.sig.inputs.len(); + + // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self + // This includes methods with default implementations + if has_valid_self_receiver && returns_self && param_count == 1 { + // Extract documentation and cfg attributes + let doc = extract_doc_comment(&method.attrs); + let cfg_attrs = extract_cfg_attributes(&method.attrs); + method_infos.push((method_name.clone(), doc, cfg_attrs)); + } + } + } + + // Generate the reflection module name + let reflection_mod_name = Ident::new( + &format!("{}_reflection", trait_name.to_string().to_snake_case()), + trait_name.span(), + ); + + // Generate wrapper functions for each method + // These wrappers use type erasure to allow runtime invocation + let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| { + let wrapper_name = Ident::new( + &format!("__wrapper_{}", method_name), + method_name.span(), + ); + quote! { + #(#cfg_attrs)* + fn #wrapper_name(value: Box) -> Box { + if let Ok(concrete) = value.downcast::() { + Box::new(concrete.#method_name()) + } else { + panic!("Type mismatch in reflection wrapper"); + } + } + } + }); + + // Generate method info entries + let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| { + let method_name_str = method_name.to_string(); + let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span()); + let doc_expr = match doc { + Some(doc_str) => quote! { Some(#doc_str) }, + None => quote! { None }, + }; + quote! { + #(#cfg_attrs)* + #inspector_reflection_path::FunctionReflection { + name: #method_name_str, + function: #wrapper_name::, + documentation: #doc_expr, + _type: ::std::marker::PhantomData, + } + } + }); + + // Generate the complete output + let output = quote! { + #trait_item + + /// Implements function reflection + #vis mod #reflection_mod_name { + use super::*; + + #(#wrapper_functions)* + + /// Get all reflectable methods for a concrete type implementing the trait + pub fn methods() -> Vec<#inspector_reflection_path::FunctionReflection> { + vec![ + #(#method_info_entries),* + ] + } + + /// Find a method by name for a concrete type implementing the trait + pub fn find_method(name: &str) -> Option<#inspector_reflection_path::FunctionReflection> { + methods::().into_iter().find(|m| m.name == name) + } + } + }; + + TokenStream::from(output) +} + +fn extract_doc_comment(attrs: &[Attribute]) -> Option { + let mut doc_lines = Vec::new(); + + for attr in attrs { + if attr.path().is_ident("doc") { + if let Meta::NameValue(meta) = &attr.meta { + if let Expr::Lit(expr_lit) = &meta.value { + if let Lit::Str(lit_str) = &expr_lit.lit { + let line = lit_str.value(); + let line = line.strip_prefix(' ').unwrap_or(&line); + doc_lines.push(line.to_string()); + } + } + } + } + } + + if doc_lines.is_empty() { + None + } else { + Some(doc_lines.join("\n")) + } +} + +fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect() +} + +fn is_called_from_gpui_crate(_span: Span) -> bool { + // Check if we're being called from within the gpui crate by examining the call site + // This is a heuristic approach - we check if the current crate name is "gpui" + std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui") +} + +struct MacroExpander; + +impl VisitMut for MacroExpander { + fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) { + let mut expanded_items = Vec::new(); + let mut items_to_keep = Vec::new(); + + for item in trait_item.items.drain(..) { + match item { + TraitItem::Macro(macro_item) => { + // Try to expand known macros + if let Some(expanded) = try_expand_macro(¯o_item) { + expanded_items.extend(expanded); + } else { + // Keep unknown macros as-is + items_to_keep.push(TraitItem::Macro(macro_item)); + } + } + other => { + items_to_keep.push(other); + } + } + } + + // Rebuild the items list with expanded content first, then original items + trait_item.items = expanded_items; + trait_item.items.extend(items_to_keep); + + // Continue visiting + visit_mut::visit_item_trait_mut(self, trait_item); + } +} + +fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option> { + let path = ¯o_item.mac.path; + + // Check if this is one of our known style macros + let macro_name = path_to_string(path); + + // Handle the known macros by calling their implementations + match macro_name.as_str() { + "gpui_macros::style_helpers" | "style_helpers" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::style_helpers(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::visibility_style_methods" | "visibility_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::margin_style_methods" | "margin_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::padding_style_methods" | "padding_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::position_style_methods" | "position_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::position_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::overflow_style_methods" | "overflow_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::cursor_style_methods" | "cursor_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::border_style_methods" | "border_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::border_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => { + let tokens = macro_item.mac.tokens.clone(); + let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens)); + parse_expanded_items(expanded) + } + _ => None, + } +} + +fn path_to_string(path: &Path) -> String { + path.segments + .iter() + .map(|seg| seg.ident.to_string()) + .collect::>() + .join("::") +} + +fn parse_expanded_items(expanded: TokenStream) -> Option> { + let tokens = TokenStream2::from(expanded); + + // Try to parse the expanded tokens as trait items + // We need to wrap them in a dummy trait to parse properly + let dummy_trait: ItemTrait = parse_quote! { + trait Dummy { + #tokens + } + }; + + Some(dummy_trait.items) +} diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index f753a5e46f..54c8e40d0f 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -6,6 +6,9 @@ mod register_action; mod styles; mod test; +#[cfg(any(feature = "inspector", debug_assertions))] +mod derive_inspector_reflection; + use proc_macro::TokenStream; use syn::{DeriveInput, Ident}; @@ -178,6 +181,28 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { test::test(args, function) } +/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides +/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`. +/// This is used by the inspector so that it can use the builder methods in `Styled` and +/// `StyledExt`. +/// +/// The generated module will have the name `_reflection` and contain the +/// following functions: +/// +/// ```ignore +/// pub fn methods::() -> Vec>; +/// +/// pub fn find_method::() -> Option>; +/// ``` +/// +/// The `invoke` method on `FunctionReflection` will run the method. `FunctionReflection` also +/// provides the method's documentation. +#[cfg(any(feature = "inspector", debug_assertions))] +#[proc_macro_attribute] +pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { + derive_inspector_reflection::derive_inspector_reflection(_args, input) +} + pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option { match &ast.data { syn::Data::Struct(data_struct) => data_struct diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs new file mode 100644 index 0000000000..522c0a62c4 --- /dev/null +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -0,0 +1,148 @@ +//! This code was generated using Zed Agent with Claude Opus 4. + +use gpui_macros::derive_inspector_reflection; + +#[derive_inspector_reflection] +trait Transform: Clone { + /// Doubles the value + fn double(self) -> Self; + + /// Triples the value + fn triple(self) -> Self; + + /// Increments the value by one + /// + /// This method has a default implementation + fn increment(self) -> Self { + // Default implementation + self.add_one() + } + + /// Quadruples the value by doubling twice + fn quadruple(self) -> Self { + // Default implementation with mut self + self.double().double() + } + + // These methods will be filtered out: + #[allow(dead_code)] + fn add(&self, other: &Self) -> Self; + #[allow(dead_code)] + fn set_value(&mut self, value: i32); + #[allow(dead_code)] + fn get_value(&self) -> i32; + + /// Adds one to the value + fn add_one(self) -> Self; + + /// cfg attributes are respected + #[cfg(all())] + fn cfg_included(self) -> Self; + + #[cfg(any())] + fn cfg_omitted(self) -> Self; +} + +#[derive(Debug, Clone, PartialEq)] +struct Number(i32); + +impl Transform for Number { + fn double(self) -> Self { + Number(self.0 * 2) + } + + fn triple(self) -> Self { + Number(self.0 * 3) + } + + fn add(&self, other: &Self) -> Self { + Number(self.0 + other.0) + } + + fn set_value(&mut self, value: i32) { + self.0 = value; + } + + fn get_value(&self) -> i32 { + self.0 + } + + fn add_one(self) -> Self { + Number(self.0 + 1) + } + + fn cfg_included(self) -> Self { + Number(self.0) + } +} + +#[test] +fn test_derive_inspector_reflection() { + use transform_reflection::*; + + // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self + let methods = methods::(); + + assert_eq!(methods.len(), 6); + let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); + assert!(method_names.contains(&"double")); + assert!(method_names.contains(&"triple")); + assert!(method_names.contains(&"increment")); + assert!(method_names.contains(&"quadruple")); + assert!(method_names.contains(&"add_one")); + assert!(method_names.contains(&"cfg_included")); + + // Invoke methods by name + let num = Number(5); + + let doubled = find_method::("double").unwrap().invoke(num.clone()); + assert_eq!(doubled, Number(10)); + + let tripled = find_method::("triple").unwrap().invoke(num.clone()); + assert_eq!(tripled, Number(15)); + + let incremented = find_method::("increment") + .unwrap() + .invoke(num.clone()); + assert_eq!(incremented, Number(6)); + + let quadrupled = find_method::("quadruple") + .unwrap() + .invoke(num.clone()); + assert_eq!(quadrupled, Number(20)); + + // Try to invoke a non-existent method + let result = find_method::("nonexistent"); + assert!(result.is_none()); + + // Chain operations + let num = Number(10); + let result = find_method::("double") + .map(|m| m.invoke(num)) + .and_then(|n| find_method::("increment").map(|m| m.invoke(n))) + .and_then(|n| find_method::("triple").map(|m| m.invoke(n))); + + assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63 + + // Test documentationumentation capture + let double_method = find_method::("double").unwrap(); + assert_eq!(double_method.documentation, Some("Doubles the value")); + + let triple_method = find_method::("triple").unwrap(); + assert_eq!(triple_method.documentation, Some("Triples the value")); + + let increment_method = find_method::("increment").unwrap(); + assert_eq!( + increment_method.documentation, + Some("Increments the value by one\n\nThis method has a default implementation") + ); + + let quadruple_method = find_method::("quadruple").unwrap(); + assert_eq!( + quadruple_method.documentation, + Some("Quadruples the value by doubling twice") + ); + + let add_one_method = find_method::("add_one").unwrap(); + assert_eq!(add_one_method.documentation, Some("Adds one to the value")); +} diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index 083651a40d..8e55a8a477 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -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 diff --git a/crates/inspector_ui/README.md b/crates/inspector_ui/README.md index a134965624..5c720dfea2 100644 --- a/crates/inspector_ui/README.md +++ b/crates/inspector_ui/README.md @@ -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)>`, 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>` 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. diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index 950daf8b1f..16396fc586 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -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, inspector_id: Option, - state: Option, - style_buffer: Option>, - style_editor: Option>, - last_error: Option, + inspector_state: Option, + /// 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, + /// Currently selected completion. + rust_completion: Option, + /// Range that will be replaced by the completion if selected. + rust_completion_replace_range: Option>, +} + +enum State { + Loading, + BuffersLoaded { + rust_style_buffer: Entity, + json_style_buffer: Entity, + }, + Ready { + rust_style_buffer: Entity, + rust_style_editor: Entity, + json_style_buffer: Entity, + json_style_editor: Entity, + }, + LoadError { + message: SharedString, + }, } impl DivInspector { @@ -29,32 +67,402 @@ impl DivInspector { window: &mut Window, cx: &mut Context, ) -> 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, - this: WeakEntity, + pub fn update_inspected_element( + &mut self, + id: &InspectorElementId, + inspector_state: DivInspectorState, + window: &mut Window, + cx: &mut Context, + ) { + 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::(&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::( + 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, + json_style_buffer: &Entity, + cx: &mut App, + ) -> Result { + 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, + cx: &mut Context, + ) { + 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, + rust_style_buffer: &Entity, + cx: &mut Context, + ) { + 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>) { + 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::(); + let after_text = snapshot + .text_for_range( + completion_range.end.to_offset(&snapshot) + ..snapshot.clip_offset(usize::MAX, Bias::Left), + ) + .collect::(); + 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::>(); + 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::>() + }; + + 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>, + rust_style_buffer: &mut Buffer, + snapshot: &BufferSnapshot, + cx: &mut Context, + ) { + 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, + project: &Entity, cx: &mut AsyncWindowContext, - ) -> Result<()> { + ) -> Result> { 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, window: &mut Window, cx: &mut Context, - ) { - 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 { + 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::( - 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, FunctionReflection)>> = + LazyLock::new(|| { + // Include StyledExt methods first so that those methods take precedence. + styled_ext_reflection::methods::() + .into_iter() + .chain(styled_reflection::methods::()) + .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, +} + +impl CompletionProvider for RustStyleCompletionProvider { + fn completions( + &self, + _excerpt_id: ExcerptId, + buffer: &Entity, + position: Anchor, + _: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> Task>>> { + 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, + _completion_indices: Vec, + _completions: Rc>>, + _cx: &mut Context, + ) -> Task> { + Task::ready(Ok(true)) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + _: &str, + _: bool, + cx: &mut Context, + ) -> 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> { + 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 + } +} diff --git a/crates/inspector_ui/src/inspector.rs b/crates/inspector_ui/src/inspector.rs index dff83cbceb..8d24b93fa9 100644 --- a/crates/inspector_ui/src/inspector.rs +++ b/crates/inspector_ui/src/inspector.rs @@ -24,7 +24,7 @@ pub fn init(app_state: Arc, 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", )) diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index 3c03504653..3f6b45cc12 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -66,7 +66,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }) .collect(); - // Create trait bound that each wrapped type must implement Clone // & Default + // Create trait bound that each wrapped type must implement Clone let type_param_bounds: Vec<_> = wrapped_types .iter() .map(|ty| { @@ -273,6 +273,116 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }) .collect(); + let refineable_is_superset_conditions: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); + + if is_refineable { + quote! { + if !self.#name.is_superset_of(&refinement.#name) { + return false; + } + } + } else if is_optional { + quote! { + if refinement.#name.is_some() && &self.#name != &refinement.#name { + return false; + } + } + } else { + quote! { + if let Some(refinement_value) = &refinement.#name { + if &self.#name != refinement_value { + return false; + } + } + } + } + }) + .collect(); + + let refinement_is_superset_conditions: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + + if is_refineable { + quote! { + if !self.#name.is_superset_of(&refinement.#name) { + return false; + } + } + } else { + quote! { + if refinement.#name.is_some() && &self.#name != &refinement.#name { + return false; + } + } + } + }) + .collect(); + + let refineable_subtract_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + let is_optional = is_optional_field(field); + + if is_refineable { + quote! { + #name: self.#name.subtract(&refinement.#name), + } + } else if is_optional { + quote! { + #name: if &self.#name == &refinement.#name { + None + } else { + self.#name.clone() + }, + } + } else { + quote! { + #name: if let Some(refinement_value) = &refinement.#name { + if &self.#name == refinement_value { + None + } else { + Some(self.#name.clone()) + } + } else { + Some(self.#name.clone()) + }, + } + } + }) + .collect(); + + let refinement_subtract_assignments: Vec = fields + .iter() + .map(|field| { + let name = &field.ident; + let is_refineable = is_refineable_field(field); + + if is_refineable { + quote! { + #name: self.#name.subtract(&refinement.#name), + } + } else { + quote! { + #name: if &self.#name == &refinement.#name { + None + } else { + self.#name.clone() + }, + } + } + }) + .collect(); + let mut derive_stream = quote! {}; for trait_to_derive in refinement_traits_to_derive { derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) @@ -303,6 +413,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { #( #refineable_refined_assignments )* self } + + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool + { + #( #refineable_is_superset_conditions )* + true + } + + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement + { + #refinement_ident { + #( #refineable_subtract_assignments )* + } + } } impl #impl_generics Refineable for #refinement_ident #ty_generics @@ -318,6 +441,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { #( #refinement_refined_assignments )* self } + + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool + { + #( #refinement_is_superset_conditions )* + true + } + + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement + { + #refinement_ident { + #( #refinement_subtract_assignments )* + } + } } impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics diff --git a/crates/refineable/src/refineable.rs b/crates/refineable/src/refineable.rs index f5e8f895a4..9d5da10ac7 100644 --- a/crates/refineable/src/refineable.rs +++ b/crates/refineable/src/refineable.rs @@ -1,23 +1,120 @@ pub use derive_refineable::Refineable; +/// A trait for types that can be refined with partial updates. +/// +/// The `Refineable` trait enables hierarchical configuration patterns where a base configuration +/// can be selectively overridden by refinements. This is particularly useful for styling and +/// settings, and theme hierarchies. +/// +/// # Derive Macro +/// +/// The `#[derive(Refineable)]` macro automatically generates a companion refinement type and +/// implements this trait. For a struct `Style`, it creates `StyleRefinement` where each field is +/// wrapped appropriately: +/// +/// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type +/// (e.g., `Bar` becomes `BarRefinement`) +/// - **Optional fields** (`Option`): Remain as `Option` +/// - **Regular fields**: Become `Option` +/// +/// ## Example +/// +/// ```rust +/// #[derive(Refineable, Clone, Default)] +/// struct Example { +/// color: String, +/// font_size: Option, +/// #[refineable] +/// margin: Margin, +/// } +/// +/// #[derive(Refineable, Clone, Default)] +/// struct Margin { +/// top: u32, +/// left: u32, +/// } +/// +/// +/// fn example() { +/// let mut example = Example::default(); +/// let refinement = ExampleRefinement { +/// color: Some("red".to_string()), +/// font_size: None, +/// margin: MarginRefinement { +/// top: Some(10), +/// left: None, +/// }, +/// }; +/// +/// base_style.refine(&refinement); +/// } +/// ``` +/// +/// This generates `ExampleRefinement` with: +/// - `color: Option` +/// - `font_size: Option` (unchanged) +/// - `margin: MarginRefinement` +/// +/// ## Attributes +/// +/// The derive macro supports these attributes on the struct: +/// - `#[refineable(Debug)]`: Implements `Debug` for the refinement type +/// - `#[refineable(Serialize)]`: Derives `Serialize` which skips serializing `None` +/// - `#[refineable(OtherTrait)]`: Derives additional traits on the refinement type +/// +/// Fields can be marked with: +/// - `#[refineable]`: Field is itself refineable (uses nested refinement type) pub trait Refineable: Clone { type Refinement: Refineable + IsEmpty + Default; + /// Applies the given refinement to this instance, modifying it in place. + /// + /// Only non-empty values in the refinement are applied. + /// + /// * For refineable fields, this recursively calls `refine`. + /// * For other fields, the value is replaced if present in the refinement. fn refine(&mut self, refinement: &Self::Refinement); + + /// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling + /// `refine` on it. fn refined(self, refinement: Self::Refinement) -> Self; + + /// Creates an instance from a cascade by merging all refinements atop the default value. fn from_cascade(cascade: &Cascade) -> Self where Self: Default + Sized, { Self::default().refined(cascade.merged()) } + + /// Returns `true` if this instance would contain all values from the refinement. + /// + /// For refineable fields, this recursively checks `is_superset_of`. For other fields, this + /// checks if the refinement's `Some` values match this instance's values. + fn is_superset_of(&self, refinement: &Self::Refinement) -> bool; + + /// Returns a refinement that represents the difference between this instance and the given + /// refinement. + /// + /// For refineable fields, this recursively calls `subtract`. For other fields, the field is + /// `None` if the field's value is equal to the refinement. + fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement; } pub trait IsEmpty { - /// When `true`, indicates that use applying this refinement does nothing. + /// Returns `true` if applying this refinement would have no effect. fn is_empty(&self) -> bool; } +/// A cascade of refinements that can be merged in priority order. +/// +/// A cascade maintains a sequence of optional refinements where later entries +/// take precedence over earlier ones. The first slot (index 0) is always the +/// base refinement and is guaranteed to be present. +/// +/// This is useful for implementing configuration hierarchies like CSS cascading, +/// where styles from different sources (user agent, user, author) are combined +/// with specific precedence rules. pub struct Cascade(Vec>); impl Default for Cascade { @@ -26,23 +123,43 @@ impl Default for Cascade { } } +/// A handle to a specific slot in a cascade. +/// +/// Slots are used to identify specific positions in the cascade where +/// refinements can be set or updated. #[derive(Copy, Clone)] pub struct CascadeSlot(usize); impl Cascade { + /// Reserves a new slot in the cascade and returns a handle to it. + /// + /// The new slot is initially empty (`None`) and can be populated later + /// using `set()`. pub fn reserve(&mut self) -> CascadeSlot { self.0.push(None); CascadeSlot(self.0.len() - 1) } + /// Returns a mutable reference to the base refinement (slot 0). + /// + /// The base refinement is always present and serves as the foundation + /// for the cascade. pub fn base(&mut self) -> &mut S::Refinement { self.0[0].as_mut().unwrap() } + /// Sets the refinement for a specific slot in the cascade. + /// + /// Setting a slot to `None` effectively removes it from consideration + /// during merging. pub fn set(&mut self, slot: CascadeSlot, refinement: Option) { self.0[slot.0] = refinement } + /// Merges all refinements in the cascade into a single refinement. + /// + /// Refinements are applied in order, with later slots taking precedence. + /// Empty slots (`None`) are skipped during merging. pub fn merged(&self) -> S::Refinement { let mut merged = self.0[0].clone().unwrap(); for refinement in self.0.iter().skip(1).flatten() { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 1073a483ea..da65e621ab 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -15,6 +15,7 @@ use picker::{Picker, PickerDelegate}; use release_channel::ReleaseChannel; use rope::Rope; use settings::Settings; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Duration; @@ -70,7 +71,7 @@ pub trait InlineAssistDelegate { pub fn open_rules_library( language_registry: Arc, inline_assist_delegate: Box, - make_completion_provider: Arc Box>, + make_completion_provider: Rc Rc>, prompt_to_select: Option, cx: &mut App, ) -> Task>> { @@ -146,7 +147,7 @@ pub struct RulesLibrary { picker: Entity>, pending_load: Task<()>, inline_assist_delegate: Box, - make_completion_provider: Arc Box>, + make_completion_provider: Rc Rc>, _subscriptions: Vec, } @@ -349,7 +350,7 @@ impl RulesLibrary { store: Entity, language_registry: Arc, inline_assist_delegate: Box, - make_completion_provider: Arc Box>, + make_completion_provider: Rc Rc>, rule_to_select: Option, window: &mut Window, cx: &mut Context, diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 170695b67f..625bdc62f5 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -17,6 +17,7 @@ chrono.workspace = true component.workspace = true documented.workspace = true gpui.workspace = true +gpui_macros.workspace = true icons.workspace = true itertools.workspace = true menu.workspace = true diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs index 1365abf6fd..63926070c8 100644 --- a/crates/ui/src/traits/styled_ext.rs +++ b/crates/ui/src/traits/styled_ext.rs @@ -18,6 +18,7 @@ fn elevated_borderless(this: E, cx: &mut App, index: ElevationIndex) } /// Extends [`gpui::Styled`] with Zed-specific styling methods. +#[cfg_attr(debug_assertions, gpui_macros::derive_inspector_reflection)] pub trait StyledExt: Styled + Sized { /// Horizontally stacks elements. /// diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 40f67cd62e..f73f222503 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1213,6 +1213,28 @@ pub fn word_consists_of_emojis(s: &str) -> bool { prev_end == s.len() } +/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike +/// `str::split`, this is not generic on pattern types and does not return an `Iterator`. +pub fn split_str_with_ranges(s: &str, pat: impl Fn(char) -> bool) -> Vec<(Range, &str)> { + let mut result = Vec::new(); + let mut start = 0; + + for (i, ch) in s.char_indices() { + if pat(ch) { + if i > start { + result.push((start..i, &s[start..i])); + } + start = i + ch.len_utf8(); + } + } + + if s.len() > start { + result.push((start..s.len(), &s[start..s.len()])); + } + + result +} + pub fn default() -> D { Default::default() } @@ -1639,4 +1661,20 @@ Line 3"# "这是什\n么 钢\n笔" ); } + + #[test] + fn test_split_with_ranges() { + let input = "hi"; + let result = split_str_with_ranges(input, |c| c == ' '); + + assert_eq!(result.len(), 1); + assert_eq!(result[0], (0..2, "hi")); + + let input = "héllo🦀world"; + let result = split_str_with_ranges(input, |c| c == '🦀'); + + assert_eq!(result.len(), 2); + assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes + assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes + } }