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