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

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


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

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

Release Notes:

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

3
Cargo.lock generated
View file

@ -7120,6 +7120,7 @@ name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gpui", "gpui",
"heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
@ -8162,6 +8163,7 @@ dependencies = [
"anyhow", "anyhow",
"command_palette_hooks", "command_palette_hooks",
"editor", "editor",
"fuzzy",
"gpui", "gpui",
"language", "language",
"project", "project",
@ -16827,6 +16829,7 @@ dependencies = [
"component", "component",
"documented", "documented",
"gpui", "gpui",
"gpui_macros",
"icons", "icons",
"itertools 0.14.0", "itertools 0.14.0",
"menu", "menu",

View file

@ -676,6 +676,7 @@
{ {
"bindings": { "bindings": {
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator", "ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
// Only available in debug builds: opens an element inspector for development.
"ctrl-alt-i": "dev::ToggleInspector" "ctrl-alt-i": "dev::ToggleInspector"
} }
}, },

View file

@ -736,6 +736,7 @@
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action // TODO: Move this to a dock open action
"cmd-shift-c": "collab_panel::ToggleFocus", "cmd-shift-c": "collab_panel::ToggleFocus",
// Only available in debug builds: opens an element inspector for development.
"cmd-alt-i": "dev::ToggleInspector" "cmd-alt-i": "dev::ToggleInspector"
} }
}, },

View file

@ -1,5 +1,6 @@
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::Path;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -915,8 +916,8 @@ impl AgentPanel {
open_rules_library( open_rules_library(
self.language_registry.clone(), self.language_registry.clone(),
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())), Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
Arc::new(|| { Rc::new(|| {
Box::new(SlashCommandCompletionProvider::new( Rc::new(SlashCommandCompletionProvider::new(
Arc::new(SlashCommandWorkingSet::default()), Arc::new(SlashCommandWorkingSet::default()),
None, None,
None, None,

View file

@ -1289,7 +1289,7 @@ mod tests {
.map(Entity::downgrade) .map(Entity::downgrade)
}); });
window.focus(&editor.focus_handle(cx)); 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(), workspace.downgrade(),
context_store.downgrade(), context_store.downgrade(),
None, None,

View file

@ -28,6 +28,7 @@ use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex; use parking_lot::Mutex;
use settings::Settings; use settings::Settings;
use std::cmp; use std::cmp;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::utils::WithRemSize; use ui::utils::WithRemSize;
@ -890,7 +891,7 @@ impl PromptEditor<BufferCodegen> {
let prompt_editor_entity = prompt_editor.downgrade(); let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| { 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(), workspace.clone(),
context_store.downgrade(), context_store.downgrade(),
thread_store.clone(), thread_store.clone(),
@ -1061,7 +1062,7 @@ impl PromptEditor<TerminalCodegen> {
let prompt_editor_entity = prompt_editor.downgrade(); let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| { 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(), workspace.clone(),
context_store.downgrade(), context_store.downgrade(),
thread_store.clone(), thread_store.clone(),

View file

@ -1,4 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use crate::agent_model_selector::{AgentModelSelector, ModelType}; use crate::agent_model_selector::{AgentModelSelector, ModelType};
@ -121,7 +122,7 @@ pub(crate) fn create_editor(
let editor_entity = editor.downgrade(); let editor_entity = editor.downgrade();
editor.update(cx, |editor, _| { editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new( editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace, workspace,
context_store, context_store,
Some(thread_store), Some(thread_store),

View file

@ -51,6 +51,7 @@ use std::{
cmp, cmp,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
@ -234,7 +235,7 @@ impl ContextEditor {
editor.set_show_breakpoints(false, cx); editor.set_show_breakpoints(false, cx);
editor.set_show_wrap_guides(false, cx); editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_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_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
editor.set_collaboration_hub(Box::new(project.clone())); editor.set_collaboration_hub(Box::new(project.clone()));

View file

@ -112,7 +112,7 @@ impl MessageEditor {
editor.set_show_gutter(false, cx); editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx); editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_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( editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx) MessageEditorSettings::get_global(cx)
.auto_replace_emoji_shortcode .auto_replace_emoji_shortcode

View file

@ -72,7 +72,7 @@ impl Console {
editor.set_show_gutter(false, cx); editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx); editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_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 editor
}); });

View file

@ -1,9 +1,9 @@
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior, AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, UniformListScrollHandle, Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
div, px, uniform_list,
}; };
use gpui::{AsyncWindowContext, WeakEntity};
use language::Buffer; use language::Buffer;
use language::CodeLabel; use language::CodeLabel;
use markdown::{Markdown, MarkdownElement}; use markdown::{Markdown, MarkdownElement};
@ -50,11 +50,12 @@ impl CodeContextMenu {
pub fn select_first( pub fn select_first(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> bool { ) -> bool {
if self.visible() { if self.visible() {
match self { 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), CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
} }
true true
@ -66,11 +67,12 @@ impl CodeContextMenu {
pub fn select_prev( pub fn select_prev(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> bool { ) -> bool {
if self.visible() { if self.visible() {
match self { 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), CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
} }
true true
@ -82,11 +84,12 @@ impl CodeContextMenu {
pub fn select_next( pub fn select_next(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> bool { ) -> bool {
if self.visible() { if self.visible() {
match self { 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), CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
} }
true true
@ -98,11 +101,12 @@ impl CodeContextMenu {
pub fn select_last( pub fn select_last(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> bool { ) -> bool {
if self.visible() { if self.visible() {
match self { 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), CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
} }
true true
@ -290,6 +294,7 @@ impl CompletionsMenu {
fn select_first( fn select_first(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
let index = if self.scroll_handle.y_flipped() { let index = if self.scroll_handle.y_flipped() {
@ -297,40 +302,56 @@ impl CompletionsMenu {
} else { } else {
0 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<Editor>) { fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let index = if self.scroll_handle.y_flipped() { let index = if self.scroll_handle.y_flipped() {
0 0
} else { } else {
self.entries.borrow().len() - 1 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<Editor>) { fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let index = if self.scroll_handle.y_flipped() { let index = if self.scroll_handle.y_flipped() {
self.next_match_index() self.next_match_index()
} else { } else {
self.prev_match_index() 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<Editor>) { fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let index = if self.scroll_handle.y_flipped() { let index = if self.scroll_handle.y_flipped() {
self.prev_match_index() self.prev_match_index()
} else { } else {
self.next_match_index() self.next_match_index()
}; };
self.update_selection_index(index, provider, cx); self.update_selection_index(index, provider, window, cx);
} }
fn update_selection_index( fn update_selection_index(
&mut self, &mut self,
match_index: usize, match_index: usize,
provider: Option<&dyn CompletionProvider>, provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
if self.selected_item != match_index { if self.selected_item != match_index {
@ -338,6 +359,9 @@ impl CompletionsMenu {
self.scroll_handle self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top); .scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx); self.resolve_visible_completions(provider, cx);
if let Some(provider) = provider {
self.handle_selection_changed(provider, window, cx);
}
cx.notify(); 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( pub fn resolve_visible_completions(
&mut self, &mut self,
provider: Option<&dyn CompletionProvider>, 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<Rc<dyn CompletionProvider>>,
editor: WeakEntity<Editor>,
cx: &mut AsyncWindowContext,
) {
let mut matches = if let Some(query) = query { let mut matches = if let Some(query) = query {
fuzzy::match_strings( fuzzy::match_strings(
&self.match_candidates, &self.match_candidates,
@ -761,7 +806,7 @@ impl CompletionsMenu {
query.chars().any(|c| c.is_uppercase()), query.chars().any(|c| c.is_uppercase()),
100, 100,
&Default::default(), &Default::default(),
executor, cx.background_executor().clone(),
) )
.await .await
} else { } else {
@ -822,6 +867,28 @@ impl CompletionsMenu {
self.selected_item = 0; self.selected_item = 0;
// This keeps the display consistent when y_flipped. // This keeps the display consistent when y_flipped.
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); 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();
}
} }
} }

View file

@ -77,7 +77,7 @@ use futures::{
FutureExt, FutureExt,
future::{self, Shared, join}, future::{self, Shared, join},
}; };
use fuzzy::StringMatchCandidate; use fuzzy::{StringMatch, StringMatchCandidate};
use ::git::blame::BlameEntry; use ::git::blame::BlameEntry;
use ::git::{Restore, blame::ParsedCommitMessage}; use ::git::{Restore, blame::ParsedCommitMessage};
@ -912,7 +912,7 @@ pub struct Editor {
// TODO: make this a access method // TODO: make this a access method
pub project: Option<Entity<Project>>, pub project: Option<Entity<Project>>,
semantics_provider: Option<Rc<dyn SemanticsProvider>>, semantics_provider: Option<Rc<dyn SemanticsProvider>>,
completion_provider: Option<Box<dyn CompletionProvider>>, completion_provider: Option<Rc<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>, collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Entity<BlinkManager>, blink_manager: Entity<BlinkManager>,
show_cursor_names: bool, show_cursor_names: bool,
@ -1755,7 +1755,7 @@ impl Editor {
soft_wrap_mode_override, soft_wrap_mode_override,
diagnostics_max_severity, diagnostics_max_severity,
hard_wrap: None, 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 _), semantics_provider: project.clone().map(|project| Rc::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _), collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project, project,
@ -2374,7 +2374,7 @@ impl Editor {
self.custom_context_menu = Some(Box::new(f)) self.custom_context_menu = Some(Box::new(f))
} }
pub fn set_completion_provider(&mut self, provider: Option<Box<dyn CompletionProvider>>) { pub fn set_completion_provider(&mut self, provider: Option<Rc<dyn CompletionProvider>>) {
self.completion_provider = provider; self.completion_provider = provider;
} }
@ -2684,9 +2684,10 @@ impl Editor {
drop(context_menu); drop(context_menu);
let query = Self::completion_query(buffer, cursor_position); 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 completion_menu
.filter(query.as_deref(), cx.background_executor().clone()) .filter(query.as_deref(), completion_provider, this.clone(), cx)
.await; .await;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@ -4960,15 +4961,16 @@ impl Editor {
let word_search_range = buffer_snapshot.point_to_offset(min_word_search) let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
..buffer_snapshot.point_to_offset(max_word_search); ..buffer_snapshot.point_to_offset(max_word_search);
let provider = self let provider = if ignore_completion_provider {
.completion_provider None
.as_ref() } else {
.filter(|_| !ignore_completion_provider); self.completion_provider.clone()
};
let skip_digits = query let skip_digits = query
.as_ref() .as_ref()
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); .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) => { Some(provider) => {
let completions = provider.completions( let completions = provider.completions(
position.excerpt_id, position.excerpt_id,
@ -5071,7 +5073,9 @@ impl Editor {
} else { } else {
None None
}, },
cx.background_executor().clone(), provider,
editor.clone(),
cx,
) )
.await; .await;
@ -8651,6 +8655,11 @@ impl Editor {
let context_menu = self.context_menu.borrow_mut().take(); let context_menu = self.context_menu.borrow_mut().take();
self.stale_inline_completion_in_menu.take(); self.stale_inline_completion_in_menu.take();
self.update_visible_inline_completion(window, cx); 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 context_menu
} }
@ -11353,7 +11362,7 @@ impl Editor {
.context_menu .context_menu
.borrow_mut() .borrow_mut()
.as_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) .unwrap_or(false)
{ {
return; return;
@ -11477,7 +11486,7 @@ impl Editor {
.context_menu .context_menu
.borrow_mut() .borrow_mut()
.as_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) .unwrap_or(false)
{ {
return; return;
@ -11532,44 +11541,44 @@ impl Editor {
pub fn context_menu_first( pub fn context_menu_first(
&mut self, &mut self,
_: &ContextMenuFirst, _: &ContextMenuFirst,
_window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { 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( pub fn context_menu_prev(
&mut self, &mut self,
_: &ContextMenuPrevious, _: &ContextMenuPrevious,
_window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { 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( pub fn context_menu_next(
&mut self, &mut self,
_: &ContextMenuNext, _: &ContextMenuNext,
_window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { 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( pub fn context_menu_last(
&mut self, &mut self,
_: &ContextMenuLast, _: &ContextMenuLast,
_window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { 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<Editor>, cx: &mut Context<Editor>,
) -> bool; ) -> bool;
fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {}
fn sort_completions(&self) -> bool { fn sort_completions(&self) -> bool {
true true
} }

View file

@ -22,7 +22,7 @@ test-support = [
"wayland", "wayland",
"x11", "x11",
] ]
inspector = [] inspector = ["gpui_macros/inspector"]
leak-detection = ["backtrace"] leak-detection = ["backtrace"]
runtime_shaders = [] runtime_shaders = []
macos-blade = [ macos-blade = [

View file

@ -8,7 +8,7 @@ use std::{
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct BoundsTree<U> pub(crate) struct BoundsTree<U>
where where
U: Default + Clone + Debug, U: Clone + Debug + Default + PartialEq,
{ {
root: Option<usize>, root: Option<usize>,
nodes: Vec<Node<U>>, nodes: Vec<Node<U>>,
@ -17,7 +17,14 @@ where
impl<U> BoundsTree<U> impl<U> BoundsTree<U>
where where
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default, U: Clone
+ Debug
+ PartialEq
+ PartialOrd
+ Add<U, Output = U>
+ Sub<Output = U>
+ Half
+ Default,
{ {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.root = None; self.root = None;
@ -174,7 +181,7 @@ where
impl<U> Default for BoundsTree<U> impl<U> Default for BoundsTree<U>
where where
U: Default + Clone + Debug, U: Clone + Debug + Default + PartialEq,
{ {
fn default() -> Self { fn default() -> Self {
BoundsTree { BoundsTree {
@ -188,7 +195,7 @@ where
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Node<U> enum Node<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
Leaf { Leaf {
bounds: Bounds<U>, bounds: Bounds<U>,
@ -204,7 +211,7 @@ where
impl<U> Node<U> impl<U> Node<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
fn bounds(&self) -> &Bounds<U> { fn bounds(&self) -> &Bounds<U> {
match self { match self {

View file

@ -76,9 +76,9 @@ pub trait Along {
JsonSchema, JsonSchema,
Hash, Hash,
)] )]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)] #[repr(C)]
pub struct Point<T: Default + Clone + Debug> { pub struct Point<T: Clone + Debug + Default + PartialEq> {
/// The x coordinate of the point. /// The x coordinate of the point.
pub x: T, pub x: T,
/// The y coordinate of the point. /// The y coordinate of the point.
@ -104,11 +104,11 @@ pub struct Point<T: Default + Clone + Debug> {
/// assert_eq!(p.x, 10); /// assert_eq!(p.x, 10);
/// assert_eq!(p.y, 20); /// assert_eq!(p.y, 20);
/// ``` /// ```
pub const fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> { pub const fn point<T: Clone + Debug + Default + PartialEq>(x: T, y: T) -> Point<T> {
Point { x, y } Point { x, y }
} }
impl<T: Clone + Debug + Default> Point<T> { impl<T: Clone + Debug + Default + PartialEq> Point<T> {
/// Creates a new `Point` with the specified `x` and `y` coordinates. /// Creates a new `Point` with the specified `x` and `y` coordinates.
/// ///
/// # Arguments /// # Arguments
@ -145,7 +145,7 @@ impl<T: Clone + Debug + Default> Point<T> {
/// let p_float = p.map(|coord| coord as f32); /// let p_float = p.map(|coord| coord as f32);
/// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 });
/// ``` /// ```
pub fn map<U: Clone + Default + Debug>(&self, f: impl Fn(T) -> U) -> Point<U> { pub fn map<U: Clone + Debug + Default + PartialEq>(&self, f: impl Fn(T) -> U) -> Point<U> {
Point { Point {
x: f(self.x.clone()), x: f(self.x.clone()),
y: f(self.y.clone()), y: f(self.y.clone()),
@ -153,7 +153,7 @@ impl<T: Clone + Debug + Default> Point<T> {
} }
} }
impl<T: Clone + Debug + Default> Along for Point<T> { impl<T: Clone + Debug + Default + PartialEq> Along for Point<T> {
type Unit = T; type Unit = T;
fn along(&self, axis: Axis) -> T { fn along(&self, axis: Axis) -> T {
@ -177,7 +177,7 @@ impl<T: Clone + Debug + Default> Along for Point<T> {
} }
} }
impl<T: Clone + Debug + Default + Negate> Negate for Point<T> { impl<T: Clone + Debug + Default + PartialEq + Negate> Negate for Point<T> {
fn negate(self) -> Self { fn negate(self) -> Self {
self.map(Negate::negate) self.map(Negate::negate)
} }
@ -222,7 +222,7 @@ impl Point<Pixels> {
impl<T> Point<T> impl<T> Point<T>
where where
T: Sub<T, Output = T> + Debug + Clone + Default, T: Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Get the position of this point, relative to the given origin /// Get the position of this point, relative to the given origin
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> { pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
@ -235,7 +235,7 @@ where
impl<T, Rhs> Mul<Rhs> for Point<T> impl<T, Rhs> Mul<Rhs> for Point<T>
where where
T: Mul<Rhs, Output = T> + Clone + Default + Debug, T: Mul<Rhs, Output = T> + Clone + Debug + Default + PartialEq,
Rhs: Clone + Debug, Rhs: Clone + Debug,
{ {
type Output = Point<T>; type Output = Point<T>;
@ -250,7 +250,7 @@ where
impl<T, S> MulAssign<S> for Point<T> impl<T, S> MulAssign<S> for Point<T>
where where
T: Clone + Mul<S, Output = T> + Default + Debug, T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
fn mul_assign(&mut self, rhs: S) { fn mul_assign(&mut self, rhs: S) {
@ -261,7 +261,7 @@ where
impl<T, S> Div<S> for Point<T> impl<T, S> Div<S> for Point<T>
where where
T: Div<S, Output = T> + Clone + Default + Debug, T: Div<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
type Output = Self; type Output = Self;
@ -276,7 +276,7 @@ where
impl<T> Point<T> impl<T> Point<T>
where 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`. /// Returns a new point with the maximum values of each dimension from `self` and `other`.
/// ///
@ -369,7 +369,7 @@ where
} }
} }
impl<T: Clone + Default + Debug> Clone for Point<T> { impl<T: Clone + Debug + Default + PartialEq> Clone for Point<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
x: self.x.clone(), x: self.x.clone(),
@ -378,7 +378,7 @@ impl<T: Clone + Default + Debug> Clone for Point<T> {
} }
} }
impl<T: Default + Clone + Debug + Display> Display for Point<T> { impl<T: Clone + Debug + Default + PartialEq + Display> Display for Point<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y) write!(f, "({}, {})", self.x, self.y)
} }
@ -389,16 +389,16 @@ impl<T: Default + Clone + Debug + Display> Display for Point<T> {
/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. /// 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. /// 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)] #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)] #[repr(C)]
pub struct Size<T: Clone + Default + Debug> { pub struct Size<T: Clone + Debug + Default + PartialEq> {
/// The width component of the size. /// The width component of the size.
pub width: T, pub width: T,
/// The height component of the size. /// The height component of the size.
pub height: T, pub height: T,
} }
impl<T: Clone + Default + Debug> Size<T> { impl<T: Clone + Debug + Default + PartialEq> Size<T> {
/// Create a new Size, a synonym for [`size`] /// Create a new Size, a synonym for [`size`]
pub fn new(width: T, height: T) -> Self { pub fn new(width: T, height: T) -> Self {
size(width, height) size(width, height)
@ -422,14 +422,14 @@ impl<T: Clone + Default + Debug> Size<T> {
/// ``` /// ```
pub const fn size<T>(width: T, height: T) -> Size<T> pub const fn size<T>(width: T, height: T) -> Size<T>
where where
T: Clone + Default + Debug, T: Clone + Debug + Default + PartialEq,
{ {
Size { width, height } Size { width, height }
} }
impl<T> Size<T> impl<T> Size<T>
where 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<U>`. /// Applies a function to the width and height of the size, producing a new `Size<U>`.
/// ///
@ -451,7 +451,7 @@ where
/// ``` /// ```
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Size<U> pub fn map<U>(&self, f: impl Fn(T) -> U) -> Size<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
Size { Size {
width: f(self.width.clone()), width: f(self.width.clone()),
@ -462,7 +462,7 @@ where
impl<T> Size<T> impl<T> Size<T>
where where
T: Clone + Default + Debug + Half, T: Clone + Debug + Default + PartialEq + Half,
{ {
/// Compute the center point of the size.g /// Compute the center point of the size.g
pub fn center(&self) -> Point<T> { pub fn center(&self) -> Point<T> {
@ -502,7 +502,7 @@ impl Size<Pixels> {
impl<T> Along for Size<T> impl<T> Along for Size<T>
where where
T: Clone + Default + Debug, T: Clone + Debug + Default + PartialEq,
{ {
type Unit = T; type Unit = T;
@ -530,7 +530,7 @@ where
impl<T> Size<T> impl<T> Size<T>
where 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`. /// Returns a new `Size` with the maximum width and height from `self` and `other`.
/// ///
@ -595,7 +595,7 @@ where
impl<T> Sub for Size<T> impl<T> Sub for Size<T>
where where
T: Sub<Output = T> + Clone + Default + Debug, T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Size<T>; type Output = Size<T>;
@ -609,7 +609,7 @@ where
impl<T> Add for Size<T> impl<T> Add for Size<T>
where where
T: Add<Output = T> + Clone + Default + Debug, T: Add<Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Size<T>; type Output = Size<T>;
@ -623,8 +623,8 @@ where
impl<T, Rhs> Mul<Rhs> for Size<T> impl<T, Rhs> Mul<Rhs> for Size<T>
where where
T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug, T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
Rhs: Clone + Default + Debug, Rhs: Clone + Debug + Default + PartialEq,
{ {
type Output = Size<Rhs>; type Output = Size<Rhs>;
@ -638,7 +638,7 @@ where
impl<T, S> MulAssign<S> for Size<T> impl<T, S> MulAssign<S> for Size<T>
where where
T: Mul<S, Output = T> + Clone + Default + Debug, T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
fn mul_assign(&mut self, rhs: S) { fn mul_assign(&mut self, rhs: S) {
@ -647,24 +647,24 @@ where
} }
} }
impl<T> Eq for Size<T> where T: Eq + Default + Debug + Clone {} impl<T> Eq for Size<T> where T: Eq + Clone + Debug + Default + PartialEq {}
impl<T> Debug for Size<T> impl<T> Debug for Size<T>
where where
T: Clone + Default + Debug, T: Clone + Debug + Default + PartialEq,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height)
} }
} }
impl<T: Default + Clone + Debug + Display> Display for Size<T> { impl<T: Clone + Debug + Default + PartialEq + Display> Display for Size<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} × {}", self.width, self.height) write!(f, "{} × {}", self.width, self.height)
} }
} }
impl<T: Clone + Default + Debug> From<Point<T>> for Size<T> { impl<T: Clone + Debug + Default + PartialEq> From<Point<T>> for Size<T> {
fn from(point: Point<T>) -> Self { fn from(point: Point<T>) -> Self {
Self { Self {
width: point.x, width: point.x,
@ -746,7 +746,7 @@ impl Size<Length> {
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[refineable(Debug)] #[refineable(Debug)]
#[repr(C)] #[repr(C)]
pub struct Bounds<T: Clone + Default + Debug> { pub struct Bounds<T: Clone + Debug + Default + PartialEq> {
/// The origin point of this area. /// The origin point of this area.
pub origin: Point<T>, pub origin: Point<T>,
/// The size of the rectangle. /// The size of the rectangle.
@ -754,7 +754,10 @@ pub struct Bounds<T: Clone + Default + Debug> {
} }
/// Create a bounds with the given origin and size /// Create a bounds with the given origin and size
pub fn bounds<T: Clone + Default + Debug>(origin: Point<T>, size: Size<T>) -> Bounds<T> { pub fn bounds<T: Clone + Debug + Default + PartialEq>(
origin: Point<T>,
size: Size<T>,
) -> Bounds<T> {
Bounds { origin, size } Bounds { origin, size }
} }
@ -790,7 +793,7 @@ impl Bounds<Pixels> {
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Default, T: Clone + Debug + Default + PartialEq,
{ {
/// Creates a new `Bounds` with the specified origin and size. /// Creates a new `Bounds` with the specified origin and size.
/// ///
@ -809,7 +812,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Sub<Output = T> + Default, T: Sub<Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Constructs a `Bounds` from two corner points: the top left and bottom right corners. /// Constructs a `Bounds` from two corner points: the top left and bottom right corners.
/// ///
@ -875,7 +878,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Sub<T, Output = T> + Default + Half, T: Sub<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
{ {
/// Creates a new bounds centered at the given point. /// Creates a new bounds centered at the given point.
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self { pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
@ -889,7 +892,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Default, T: PartialOrd + Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Checks if this `Bounds` intersects with another `Bounds`. /// Checks if this `Bounds` intersects with another `Bounds`.
/// ///
@ -937,7 +940,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Add<T, Output = T> + Default + Half, T: Add<T, Output = T> + Half + Clone + Debug + Default + PartialEq,
{ {
/// Returns the center point of the bounds. /// Returns the center point of the bounds.
/// ///
@ -970,7 +973,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Add<T, Output = T> + Default, T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Calculates the half perimeter of a rectangle defined by the bounds. /// Calculates the half perimeter of a rectangle defined by the bounds.
/// ///
@ -997,7 +1000,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Add<T, Output = T> + Sub<Output = T> + Default, T: Add<T, Output = T> + Sub<Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Dilates the bounds by a specified amount in all directions. /// Dilates the bounds by a specified amount in all directions.
/// ///
@ -1048,7 +1051,13 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Neg<Output = T> + Default, T: Add<T, Output = T>
+ Sub<T, Output = T>
+ Neg<Output = T>
+ Clone
+ Debug
+ Default
+ PartialEq,
{ {
/// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated.
/// ///
@ -1058,7 +1067,9 @@ where
} }
} }
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> { impl<T: PartialOrd + Add<T, Output = T> + Sub<Output = T> + Clone + Debug + Default + PartialEq>
Bounds<T>
{
/// Calculates the intersection of two `Bounds` objects. /// Calculates the intersection of two `Bounds` objects.
/// ///
/// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect,
@ -1140,7 +1151,7 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Default, T: Add<T, Output = T> + Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Computes the space available within outer bounds. /// Computes the space available within outer bounds.
pub fn space_within(&self, outer: &Self) -> Edges<T> { pub fn space_within(&self, outer: &Self) -> Edges<T> {
@ -1155,9 +1166,9 @@ where
impl<T, Rhs> Mul<Rhs> for Bounds<T> impl<T, Rhs> Mul<Rhs> for Bounds<T>
where where
T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug, T: Mul<Rhs, Output = Rhs> + Clone + Debug + Default + PartialEq,
Point<T>: Mul<Rhs, Output = Point<Rhs>>, Point<T>: Mul<Rhs, Output = Point<Rhs>>,
Rhs: Clone + Default + Debug, Rhs: Clone + Debug + Default + PartialEq,
{ {
type Output = Bounds<Rhs>; type Output = Bounds<Rhs>;
@ -1171,7 +1182,7 @@ where
impl<T, S> MulAssign<S> for Bounds<T> impl<T, S> MulAssign<S> for Bounds<T>
where where
T: Mul<S, Output = T> + Clone + Default + Debug, T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
fn mul_assign(&mut self, rhs: S) { fn mul_assign(&mut self, rhs: S) {
@ -1183,7 +1194,7 @@ where
impl<T, S> Div<S> for Bounds<T> impl<T, S> Div<S> for Bounds<T>
where where
Size<T>: Div<S, Output = Size<T>>, Size<T>: Div<S, Output = Size<T>>,
T: Div<S, Output = T> + Default + Clone + Debug, T: Div<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
type Output = Self; type Output = Self;
@ -1198,7 +1209,7 @@ where
impl<T> Add<Point<T>> for Bounds<T> impl<T> Add<Point<T>> for Bounds<T>
where where
T: Add<T, Output = T> + Default + Clone + Debug, T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Self; type Output = Self;
@ -1212,7 +1223,7 @@ where
impl<T> Sub<Point<T>> for Bounds<T> impl<T> Sub<Point<T>> for Bounds<T>
where where
T: Sub<T, Output = T> + Default + Clone + Debug, T: Sub<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Self; type Output = Self;
@ -1226,7 +1237,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Add<T, Output = T> + Clone + Default + Debug, T: Add<T, Output = T> + Clone + Debug + Default + PartialEq,
{ {
/// Returns the top edge of the bounds. /// Returns the top edge of the bounds.
/// ///
@ -1365,7 +1376,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug, T: Add<T, Output = T> + PartialOrd + Clone + Debug + Default + PartialEq,
{ {
/// Checks if the given point is within the bounds. /// Checks if the given point is within the bounds.
/// ///
@ -1472,7 +1483,7 @@ where
/// ``` /// ```
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U> pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
Bounds { Bounds {
origin: self.origin.map(&f), origin: self.origin.map(&f),
@ -1531,7 +1542,7 @@ where
impl<T> Bounds<T> impl<T> Bounds<T>
where where
T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug + Sub<T, Output = T>, T: Add<T, Output = T> + Sub<T, Output = T> + PartialOrd + Clone + Debug + Default + PartialEq,
{ {
/// Convert a point to the coordinate space defined by this Bounds /// Convert a point to the coordinate space defined by this Bounds
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> { pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
@ -1545,7 +1556,7 @@ where
/// # Returns /// # Returns
/// ///
/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area.
impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> { impl<T: PartialOrd + Clone + Debug + Default + PartialEq> Bounds<T> {
/// Checks if the bounds represent an empty area. /// Checks if the bounds represent an empty area.
/// ///
/// # Returns /// # Returns
@ -1556,7 +1567,7 @@ impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> {
} }
} }
impl<T: Default + Clone + Debug + Display + Add<T, Output = T>> Display for Bounds<T> { impl<T: Clone + Debug + Default + PartialEq + Display + Add<T, Output = T>> Display for Bounds<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
@ -1651,7 +1662,7 @@ impl Bounds<DevicePixels> {
} }
} }
impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {} impl<T: Copy + Clone + Debug + Default + PartialEq> Copy for Bounds<T> {}
/// Represents the edges of a box in a 2D space, such as padding or margin. /// Represents the edges of a box in a 2D space, such as padding or margin.
/// ///
@ -1674,9 +1685,9 @@ impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
/// assert_eq!(edges.left, 40.0); /// assert_eq!(edges.left, 40.0);
/// ``` /// ```
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)] #[repr(C)]
pub struct Edges<T: Clone + Default + Debug> { pub struct Edges<T: Clone + Debug + Default + PartialEq> {
/// The size of the top edge. /// The size of the top edge.
pub top: T, pub top: T,
/// The size of the right edge. /// The size of the right edge.
@ -1689,7 +1700,7 @@ pub struct Edges<T: Clone + Default + Debug> {
impl<T> Mul for Edges<T> impl<T> Mul for Edges<T>
where where
T: Mul<Output = T> + Clone + Default + Debug, T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Self; type Output = Self;
@ -1705,7 +1716,7 @@ where
impl<T, S> MulAssign<S> for Edges<T> impl<T, S> MulAssign<S> for Edges<T>
where where
T: Mul<S, Output = T> + Clone + Default + Debug, T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
fn mul_assign(&mut self, rhs: S) { fn mul_assign(&mut self, rhs: S) {
@ -1716,9 +1727,9 @@ where
} }
} }
impl<T: Clone + Default + Debug + Copy> Copy for Edges<T> {} impl<T: Clone + Debug + Default + PartialEq + Copy> Copy for Edges<T> {}
impl<T: Clone + Default + Debug> Edges<T> { impl<T: Clone + Debug + Default + PartialEq> Edges<T> {
/// Constructs `Edges` where all sides are set to the same specified value. /// 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 /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized
@ -1776,7 +1787,7 @@ impl<T: Clone + Default + Debug> Edges<T> {
/// ``` /// ```
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Edges<U> pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Edges<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
Edges { Edges {
top: f(&self.top), 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`. /// 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)] #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)] #[repr(C)]
pub struct Corners<T: Clone + Default + Debug> { pub struct Corners<T: Clone + Debug + Default + PartialEq> {
/// The value associated with the top left corner. /// The value associated with the top left corner.
pub top_left: T, pub top_left: T,
/// The value associated with the top right corner. /// The value associated with the top right corner.
@ -2166,7 +2177,7 @@ pub struct Corners<T: Clone + Default + Debug> {
impl<T> Corners<T> impl<T> Corners<T>
where where
T: Clone + Default + Debug, T: Clone + Debug + Default + PartialEq,
{ {
/// Constructs `Corners` where all sides are set to the same specified value. /// Constructs `Corners` where all sides are set to the same specified value.
/// ///
@ -2319,7 +2330,7 @@ impl Corners<Pixels> {
} }
} }
impl<T: Div<f32, Output = T> + Ord + Clone + Default + Debug> Corners<T> { impl<T: Div<f32, Output = T> + Ord + Clone + Debug + Default + PartialEq> Corners<T> {
/// Clamps corner radii to be less than or equal to half the shortest side of a quad. /// Clamps corner radii to be less than or equal to half the shortest side of a quad.
/// ///
/// # Arguments /// # Arguments
@ -2340,7 +2351,7 @@ impl<T: Div<f32, Output = T> + Ord + Clone + Default + Debug> Corners<T> {
} }
} }
impl<T: Clone + Default + Debug> Corners<T> { impl<T: Clone + Debug + Default + PartialEq> Corners<T> {
/// Applies a function to each field of the `Corners`, producing a new `Corners<U>`. /// Applies a function to each field of the `Corners`, producing a new `Corners<U>`.
/// ///
/// This method allows for converting a `Corners<T>` to a `Corners<U>` by specifying a closure /// This method allows for converting a `Corners<T>` to a `Corners<U>` by specifying a closure
@ -2375,7 +2386,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
/// ``` /// ```
pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Corners<U> pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Corners<U>
where where
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
Corners { Corners {
top_left: f(&self.top_left), top_left: f(&self.top_left),
@ -2388,7 +2399,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
impl<T> Mul for Corners<T> impl<T> Mul for Corners<T>
where where
T: Mul<Output = T> + Clone + Default + Debug, T: Mul<Output = T> + Clone + Debug + Default + PartialEq,
{ {
type Output = Self; type Output = Self;
@ -2404,7 +2415,7 @@ where
impl<T, S> MulAssign<S> for Corners<T> impl<T, S> MulAssign<S> for Corners<T>
where where
T: Mul<S, Output = T> + Clone + Default + Debug, T: Mul<S, Output = T> + Clone + Debug + Default + PartialEq,
S: Clone, S: Clone,
{ {
fn mul_assign(&mut self, rhs: S) { fn mul_assign(&mut self, rhs: S) {
@ -2415,7 +2426,7 @@ where
} }
} }
impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {} impl<T> Copy for Corners<T> where T: Copy + Clone + Debug + Default + PartialEq {}
impl From<f32> for Corners<Pixels> { impl From<f32> for Corners<Pixels> {
fn from(val: f32) -> Self { 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. /// A length that can be defined in pixels, rems, percent of parent, or auto.
#[derive(Clone, Copy)] #[derive(Clone, Copy, PartialEq)]
pub enum Length { pub enum Length {
/// A definite length specified either in pixels, rems, or as a fraction of the parent's size. /// A definite length specified either in pixels, rems, or as a fraction of the parent's size.
Definite(DefiniteLength), Definite(DefiniteLength),
@ -3772,7 +3783,7 @@ impl IsZero for Length {
} }
} }
impl<T: IsZero + Debug + Clone + Default> IsZero for Point<T> { impl<T: IsZero + Clone + Debug + Default + PartialEq> IsZero for Point<T> {
fn is_zero(&self) -> bool { fn is_zero(&self) -> bool {
self.x.is_zero() && self.y.is_zero() self.x.is_zero() && self.y.is_zero()
} }
@ -3780,14 +3791,14 @@ impl<T: IsZero + Debug + Clone + Default> IsZero for Point<T> {
impl<T> IsZero for Size<T> impl<T> IsZero for Size<T>
where where
T: IsZero + Default + Debug + Clone, T: IsZero + Clone + Debug + Default + PartialEq,
{ {
fn is_zero(&self) -> bool { fn is_zero(&self) -> bool {
self.width.is_zero() || self.height.is_zero() self.width.is_zero() || self.height.is_zero()
} }
} }
impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> { impl<T: IsZero + Clone + Debug + Default + PartialEq> IsZero for Bounds<T> {
fn is_zero(&self) -> bool { fn is_zero(&self) -> bool {
self.size.is_zero() self.size.is_zero()
} }
@ -3795,7 +3806,7 @@ impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> {
impl<T> IsZero for Corners<T> impl<T> IsZero for Corners<T>
where where
T: IsZero + Clone + Default + Debug, T: IsZero + Clone + Debug + Default + PartialEq,
{ {
fn is_zero(&self) -> bool { fn is_zero(&self) -> bool {
self.top_left.is_zero() self.top_left.is_zero()

View file

@ -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<T> {
/// The name of the function
pub name: &'static str,
/// The method
pub function: fn(Box<dyn Any>) -> Box<dyn Any>,
/// Documentation for the function
pub documentation: Option<&'static str>,
/// `PhantomData` for the type of the argument and result
pub _type: std::marker::PhantomData<T>,
}
impl<T: 'static> FunctionReflection<T> {
/// Invoke this method on a value and return the result.
pub fn invoke(&self, value: T) -> T {
let boxed = Box::new(value) as Box<dyn Any>;
let result = (self.function)(boxed);
*result
.downcast::<T>()
.expect("Type mismatch in reflection invoke")
}
}
}

View file

@ -679,7 +679,7 @@ pub(crate) struct PathId(pub(crate) usize);
/// A line made up of a series of vertices and control points. /// A line made up of a series of vertices and control points.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Path<P: Clone + Default + Debug> { pub struct Path<P: Clone + Debug + Default + PartialEq> {
pub(crate) id: PathId, pub(crate) id: PathId,
order: DrawOrder, order: DrawOrder,
pub(crate) bounds: Bounds<P>, pub(crate) bounds: Bounds<P>,
@ -812,7 +812,7 @@ impl From<Path<ScaledPixels>> for Primitive {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[repr(C)] #[repr(C)]
pub(crate) struct PathVertex<P: Clone + Default + Debug> { pub(crate) struct PathVertex<P: Clone + Debug + Default + PartialEq> {
pub(crate) xy_position: Point<P>, pub(crate) xy_position: Point<P>,
pub(crate) st_position: Point<f32>, pub(crate) st_position: Point<f32>,
pub(crate) content_mask: ContentMask<P>, pub(crate) content_mask: ContentMask<P>,

View file

@ -140,7 +140,7 @@ impl ObjectFit {
/// The CSS styling that can be applied to an element via the `Styled` trait /// The CSS styling that can be applied to an element via the `Styled` trait
#[derive(Clone, Refineable, Debug)] #[derive(Clone, Refineable, Debug)]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Style { pub struct Style {
/// What layout strategy should be used? /// What layout strategy should be used?
pub display: Display, pub display: Display,
@ -286,7 +286,7 @@ pub enum Visibility {
} }
/// The possible values of the box-shadow property /// The possible values of the box-shadow property
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct BoxShadow { pub struct BoxShadow {
/// What color should the shadow have? /// What color should the shadow have?
pub color: Hsla, pub color: Hsla,
@ -332,7 +332,7 @@ pub enum TextAlign {
/// The properties that can be used to style text in GPUI /// The properties that can be used to style text in GPUI
#[derive(Refineable, Clone, Debug, PartialEq)] #[derive(Refineable, Clone, Debug, PartialEq)]
#[refineable(Debug, Serialize, Deserialize, JsonSchema)] #[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextStyle { pub struct TextStyle {
/// The color of the text /// The color of the text
pub color: Hsla, pub color: Hsla,
@ -794,7 +794,7 @@ pub struct StrikethroughStyle {
} }
/// The kinds of fill that can be applied to a shape. /// 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 { pub enum Fill {
/// A solid color fill. /// A solid color fill.
Color(Background), Color(Background),

View file

@ -14,6 +14,10 @@ const ELLIPSIS: SharedString = SharedString::new_static("…");
/// A trait for elements that can be styled. /// A trait for elements that can be styled.
/// Use this to opt-in to a utility CSS-like styling API. /// 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 { pub trait Styled: Sized {
/// Returns a reference to the style memory of this element. /// Returns a reference to the style memory of this element.
fn style(&mut self) -> &mut StyleRefinement; fn style(&mut self) -> &mut StyleRefinement;

View file

@ -359,7 +359,7 @@ impl ToTaffy<taffy::style::LengthPercentage> for AbsoluteLength {
impl<T, T2> From<TaffyPoint<T>> for Point<T2> impl<T, T2> From<TaffyPoint<T>> for Point<T2>
where where
T: Into<T2>, T: Into<T2>,
T2: Clone + Default + Debug, T2: Clone + Debug + Default + PartialEq,
{ {
fn from(point: TaffyPoint<T>) -> Point<T2> { fn from(point: TaffyPoint<T>) -> Point<T2> {
Point { Point {
@ -371,7 +371,7 @@ where
impl<T, T2> From<Point<T>> for TaffyPoint<T2> impl<T, T2> From<Point<T>> for TaffyPoint<T2>
where where
T: Into<T2> + Clone + Default + Debug, T: Into<T2> + Clone + Debug + Default + PartialEq,
{ {
fn from(val: Point<T>) -> Self { fn from(val: Point<T>) -> Self {
TaffyPoint { TaffyPoint {
@ -383,7 +383,7 @@ where
impl<T, U> ToTaffy<TaffySize<U>> for Size<T> impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
where where
T: ToTaffy<U> + Clone + Default + Debug, T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{ {
fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> { fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
TaffySize { TaffySize {
@ -395,7 +395,7 @@ where
impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T> impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
where where
T: ToTaffy<U> + Clone + Default + Debug, T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{ {
fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> { fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
TaffyRect { TaffyRect {
@ -410,7 +410,7 @@ where
impl<T, U> From<TaffySize<T>> for Size<U> impl<T, U> From<TaffySize<T>> for Size<U>
where where
T: Into<U>, T: Into<U>,
U: Clone + Default + Debug, U: Clone + Debug + Default + PartialEq,
{ {
fn from(taffy_size: TaffySize<T>) -> Self { fn from(taffy_size: TaffySize<T>) -> Self {
Size { Size {
@ -422,7 +422,7 @@ where
impl<T, U> From<Size<T>> for TaffySize<U> impl<T, U> From<Size<T>> for TaffySize<U>
where where
T: Into<U> + Clone + Default + Debug, T: Into<U> + Clone + Debug + Default + PartialEq,
{ {
fn from(size: Size<T>) -> Self { fn from(size: Size<T>) -> Self {
TaffySize { TaffySize {

View file

@ -979,7 +979,7 @@ pub(crate) struct DispatchEventResult {
/// to leave room to support more complex shapes in the future. /// to leave room to support more complex shapes in the future.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)] #[repr(C)]
pub struct ContentMask<P: Clone + Default + Debug> { pub struct ContentMask<P: Clone + Debug + Default + PartialEq> {
/// The bounds /// The bounds
pub bounds: Bounds<P>, pub bounds: Bounds<P>,
} }

View file

@ -8,16 +8,20 @@ license = "Apache-2.0"
[lints] [lints]
workspace = true workspace = true
[features]
inspector = []
[lib] [lib]
path = "src/gpui_macros.rs" path = "src/gpui_macros.rs"
proc-macro = true proc-macro = true
doctest = true doctest = true
[dependencies] [dependencies]
heck.workspace = true
proc-macro2.workspace = true proc-macro2.workspace = true
quote.workspace = true quote.workspace = true
syn.workspace = true syn.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[dev-dependencies] [dev-dependencies]
gpui.workspace = true gpui = { workspace = true, features = ["inspector"] }

View file

@ -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<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
if let Ok(concrete) = value.downcast::<T>() {
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::<T>,
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<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
vec![
#(#method_info_entries),*
]
}
/// Find a method by name for a concrete type implementing the trait
pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
methods::<T>().into_iter().find(|m| m.name == name)
}
}
};
TokenStream::from(output)
}
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
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<Attribute> {
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(&macro_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<Vec<TraitItem>> {
let path = &macro_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::<Vec<_>>()
.join("::")
}
fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
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)
}

View file

@ -6,6 +6,9 @@ mod register_action;
mod styles; mod styles;
mod test; mod test;
#[cfg(any(feature = "inspector", debug_assertions))]
mod derive_inspector_reflection;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use syn::{DeriveInput, Ident}; use syn::{DeriveInput, Ident};
@ -178,6 +181,28 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function) 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 `<snake_case_trait_name>_reflection` and contain the
/// following functions:
///
/// ```ignore
/// pub fn methods::<T: TheTrait + 'static>() -> Vec<gpui::inspector_reflection::FunctionReflection<T>>;
///
/// pub fn find_method::<T: TheTrait + 'static>() -> Option<gpui::inspector_reflection::FunctionReflection<T>>;
/// ```
///
/// 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<Ident> { pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option<Ident> {
match &ast.data { match &ast.data {
syn::Data::Struct(data_struct) => data_struct syn::Data::Struct(data_struct) => data_struct

View file

@ -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::<Number>();
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::<Number>("double").unwrap().invoke(num.clone());
assert_eq!(doubled, Number(10));
let tripled = find_method::<Number>("triple").unwrap().invoke(num.clone());
assert_eq!(tripled, Number(15));
let incremented = find_method::<Number>("increment")
.unwrap()
.invoke(num.clone());
assert_eq!(incremented, Number(6));
let quadrupled = find_method::<Number>("quadruple")
.unwrap()
.invoke(num.clone());
assert_eq!(quadrupled, Number(20));
// Try to invoke a non-existent method
let result = find_method::<Number>("nonexistent");
assert!(result.is_none());
// Chain operations
let num = Number(10);
let result = find_method::<Number>("double")
.map(|m| m.invoke(num))
.and_then(|n| find_method::<Number>("increment").map(|m| m.invoke(n)))
.and_then(|n| find_method::<Number>("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::<Number>("double").unwrap();
assert_eq!(double_method.documentation, Some("Doubles the value"));
let triple_method = find_method::<Number>("triple").unwrap();
assert_eq!(triple_method.documentation, Some("Triples the value"));
let increment_method = find_method::<Number>("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::<Number>("quadruple").unwrap();
assert_eq!(
quadruple_method.documentation,
Some("Quadruples the value by doubling twice")
);
let add_one_method = find_method::<Number>("add_one").unwrap();
assert_eq!(add_one_method.documentation, Some("Adds one to the value"));
}

View file

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

View file

@ -1,8 +1,6 @@
# Inspector # Inspector
This is a tool for inspecting and manipulating rendered elements in Zed. It is 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.
only available in debug builds. Use the `dev::ToggleInspector` action to toggle
inspector mode and click on UI elements to inspect them.
# Current features # Current features
@ -10,44 +8,72 @@ inspector mode and click on UI elements to inspect them.
* Temporary manipulation of the selected element. * 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. * Navigation to code that constructed the element.
# Known bugs # Known bugs
* The style inspector buffer will leak memory over time due to building up ## JSON style editor undo history doesn't get reset
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. 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 # 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. * 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. * 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? * 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. * 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. * Tracking of more source locations - currently the source location is often in a ui compoenent. Ideally this would have a way for the components to indicate that they are probably not the source location the user is looking for.
- Could have `InspectorElementId` be `Vec<(ElementId, Option<Location>)>`, but if there are multiple code paths that construct the same element this would cause them to be considered different.
- Probably better to have a separate `Vec<Option<Location>>` that uses the same indices as `GlobalElementId`.
## Persistent modification ## Persistent modification
Currently, element modifications disappear when picker mode is started. Handling this well is tricky. Potential features: 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. * 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 # 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. Currently the inspector has special rendering in the UI, but maybe it could just be a workspace item.

View file

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

View file

@ -24,7 +24,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
}); });
}); });
// Project used for editor buffers + LSP support // Project used for editor buffers with LSP support
let project = project::Project::local( let project = project::Project::local(
app_state.client.clone(), app_state.client.clone(),
app_state.node_runtime.clone(), app_state.node_runtime.clone(),
@ -57,14 +57,12 @@ fn render_inspector(
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let inspector_id = inspector.active_element_id(); let inspector_id = inspector.active_element_id();
v_flex() v_flex()
.id("gpui-inspector")
.size_full() .size_full()
.bg(colors.panel_background) .bg(colors.panel_background)
.text_color(colors.text) .text_color(colors.text)
.font(ui_font) .font(ui_font)
.border_l_1() .border_l_1()
.border_color(colors.border) .border_color(colors.border)
.overflow_y_scroll()
.child( .child(
h_flex() h_flex()
.p_2() .p_2()
@ -89,6 +87,8 @@ fn render_inspector(
) )
.child( .child(
v_flex() v_flex()
.id("gpui-inspector-content")
.overflow_y_scroll()
.p_2() .p_2()
.gap_2() .gap_2()
.when_some(inspector_id, |this, inspector_id| { .when_some(inspector_id, |this, inspector_id| {
@ -101,26 +101,32 @@ fn render_inspector(
fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div { fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
let source_location = inspector_id.path.source_location; 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() v_flex()
.child(Label::new("Element ID").size(LabelSize::Large)) .child(Label::new("Element ID").size(LabelSize::Large))
.when(inspector_id.instance_id != 0, |this| { .child(
this.child( div()
div() .id("instance-id")
.id("instance-id") .text_ui(cx)
.text_ui(cx) .tooltip(Tooltip::text(
.tooltip(Tooltip::text( "Disambiguates elements from the same source location",
"Disambiguates elements from the same source location", ))
)) .child(format!("Instance {}", inspector_id.instance_id)),
.child(format!("Instance {}", inspector_id.instance_id)), )
)
})
.child( .child(
div() div()
.id("source-location") .id("source-location")
.text_ui(cx) .text_ui(cx)
.bg(cx.theme().colors().editor_foreground.opacity(0.025)) .bg(cx.theme().colors().editor_foreground.opacity(0.025))
.underline() .underline()
.child(format!("{}", source_location)) .child(source_location_string)
.tooltip(Tooltip::text("Click to open by running zed cli")) .tooltip(Tooltip::text("Click to open by running zed cli"))
.on_click(move |_, _window, cx| { .on_click(move |_, _window, cx| {
cx.background_spawn(open_zed_source_location(source_location)) cx.background_spawn(open_zed_source_location(source_location))
@ -131,7 +137,7 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
div() div()
.id("global-id") .id("global-id")
.text_ui(cx) .text_ui(cx)
.min_h_12() .min_h_20()
.tooltip(Tooltip::text( .tooltip(Tooltip::text(
"GlobalElementId of the nearest ancestor with an ID", "GlobalElementId of the nearest ancestor with an ID",
)) ))

View file

@ -66,7 +66,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
}) })
.collect(); .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 let type_param_bounds: Vec<_> = wrapped_types
.iter() .iter()
.map(|ty| { .map(|ty| {
@ -273,6 +273,116 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
}) })
.collect(); .collect();
let refineable_is_superset_conditions: Vec<TokenStream2> = 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<TokenStream2> = 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<TokenStream2> = 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<TokenStream2> = 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! {}; let mut derive_stream = quote! {};
for trait_to_derive in refinement_traits_to_derive { for trait_to_derive in refinement_traits_to_derive {
derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
@ -303,6 +413,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refineable_refined_assignments )* #( #refineable_refined_assignments )*
self 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 impl #impl_generics Refineable for #refinement_ident #ty_generics
@ -318,6 +441,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refinement_refined_assignments )* #( #refinement_refined_assignments )*
self 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 impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics

View file

@ -1,23 +1,120 @@
pub use derive_refineable::Refineable; 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<T>`): Remain as `Option<T>`
/// - **Regular fields**: Become `Option<T>`
///
/// ## Example
///
/// ```rust
/// #[derive(Refineable, Clone, Default)]
/// struct Example {
/// color: String,
/// font_size: Option<u32>,
/// #[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<String>`
/// - `font_size: Option<u32>` (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 { pub trait Refineable: Clone {
type Refinement: Refineable<Refinement = Self::Refinement> + IsEmpty + Default; type Refinement: Refineable<Refinement = Self::Refinement> + 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); 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; 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>) -> Self fn from_cascade(cascade: &Cascade<Self>) -> Self
where where
Self: Default + Sized, Self: Default + Sized,
{ {
Self::default().refined(cascade.merged()) 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 { 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; 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<S: Refineable>(Vec<Option<S::Refinement>>); pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
impl<S: Refineable + Default> Default for Cascade<S> { impl<S: Refineable + Default> Default for Cascade<S> {
@ -26,23 +123,43 @@ impl<S: Refineable + Default> Default for Cascade<S> {
} }
} }
/// 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)] #[derive(Copy, Clone)]
pub struct CascadeSlot(usize); pub struct CascadeSlot(usize);
impl<S: Refineable + Default> Cascade<S> { impl<S: Refineable + Default> Cascade<S> {
/// 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 { pub fn reserve(&mut self) -> CascadeSlot {
self.0.push(None); self.0.push(None);
CascadeSlot(self.0.len() - 1) 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 { pub fn base(&mut self) -> &mut S::Refinement {
self.0[0].as_mut().unwrap() 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<S::Refinement>) { pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
self.0[slot.0] = refinement 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 { pub fn merged(&self) -> S::Refinement {
let mut merged = self.0[0].clone().unwrap(); let mut merged = self.0[0].clone().unwrap();
for refinement in self.0.iter().skip(1).flatten() { for refinement in self.0.iter().skip(1).flatten() {

View file

@ -15,6 +15,7 @@ use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel; use release_channel::ReleaseChannel;
use rope::Rope; use rope::Rope;
use settings::Settings; use settings::Settings;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::time::Duration; use std::time::Duration;
@ -70,7 +71,7 @@ pub trait InlineAssistDelegate {
pub fn open_rules_library( pub fn open_rules_library(
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>, inline_assist_delegate: Box<dyn InlineAssistDelegate>,
make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>, make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
prompt_to_select: Option<PromptId>, prompt_to_select: Option<PromptId>,
cx: &mut App, cx: &mut App,
) -> Task<Result<WindowHandle<RulesLibrary>>> { ) -> Task<Result<WindowHandle<RulesLibrary>>> {
@ -146,7 +147,7 @@ pub struct RulesLibrary {
picker: Entity<Picker<RulePickerDelegate>>, picker: Entity<Picker<RulePickerDelegate>>,
pending_load: Task<()>, pending_load: Task<()>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>, inline_assist_delegate: Box<dyn InlineAssistDelegate>,
make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>, make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -349,7 +350,7 @@ impl RulesLibrary {
store: Entity<PromptStore>, store: Entity<PromptStore>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>, inline_assist_delegate: Box<dyn InlineAssistDelegate>,
make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>, make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
rule_to_select: Option<PromptId>, rule_to_select: Option<PromptId>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,

View file

@ -17,6 +17,7 @@ chrono.workspace = true
component.workspace = true component.workspace = true
documented.workspace = true documented.workspace = true
gpui.workspace = true gpui.workspace = true
gpui_macros.workspace = true
icons.workspace = true icons.workspace = true
itertools.workspace = true itertools.workspace = true
menu.workspace = true menu.workspace = true

View file

@ -18,6 +18,7 @@ fn elevated_borderless<E: Styled>(this: E, cx: &mut App, index: ElevationIndex)
} }
/// Extends [`gpui::Styled`] with Zed-specific styling methods. /// Extends [`gpui::Styled`] with Zed-specific styling methods.
#[cfg_attr(debug_assertions, gpui_macros::derive_inspector_reflection)]
pub trait StyledExt: Styled + Sized { pub trait StyledExt: Styled + Sized {
/// Horizontally stacks elements. /// Horizontally stacks elements.
/// ///

View file

@ -1213,6 +1213,28 @@ pub fn word_consists_of_emojis(s: &str) -> bool {
prev_end == s.len() 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<usize>, &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>() -> D { pub fn default<D: Default>() -> D {
Default::default() Default::default()
} }
@ -1639,4 +1661,20 @@ Line 3"#
"这是什\n么 钢\n" "这是什\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
}
} }