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

View file

@ -66,7 +66,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
})
.collect();
// Create trait bound that each wrapped type must implement Clone // & Default
// Create trait bound that each wrapped type must implement Clone
let type_param_bounds: Vec<_> = wrapped_types
.iter()
.map(|ty| {
@ -273,6 +273,116 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
})
.collect();
let refineable_is_superset_conditions: Vec<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! {};
for trait_to_derive in refinement_traits_to_derive {
derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
@ -303,6 +413,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refineable_refined_assignments )*
self
}
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
{
#( #refineable_is_superset_conditions )*
true
}
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
{
#refinement_ident {
#( #refineable_subtract_assignments )*
}
}
}
impl #impl_generics Refineable for #refinement_ident #ty_generics
@ -318,6 +441,19 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
#( #refinement_refined_assignments )*
self
}
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
{
#( #refinement_is_superset_conditions )*
true
}
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
{
#refinement_ident {
#( #refinement_subtract_assignments )*
}
}
}
impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics

View file

@ -1,23 +1,120 @@
pub use derive_refineable::Refineable;
/// A trait for types that can be refined with partial updates.
///
/// The `Refineable` trait enables hierarchical configuration patterns where a base configuration
/// can be selectively overridden by refinements. This is particularly useful for styling and
/// settings, and theme hierarchies.
///
/// # Derive Macro
///
/// The `#[derive(Refineable)]` macro automatically generates a companion refinement type and
/// implements this trait. For a struct `Style`, it creates `StyleRefinement` where each field is
/// wrapped appropriately:
///
/// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type
/// (e.g., `Bar` becomes `BarRefinement`)
/// - **Optional fields** (`Option<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 {
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);
/// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling
/// `refine` on it.
fn refined(self, refinement: Self::Refinement) -> Self;
/// Creates an instance from a cascade by merging all refinements atop the default value.
fn from_cascade(cascade: &Cascade<Self>) -> Self
where
Self: Default + Sized,
{
Self::default().refined(cascade.merged())
}
/// Returns `true` if this instance would contain all values from the refinement.
///
/// For refineable fields, this recursively checks `is_superset_of`. For other fields, this
/// checks if the refinement's `Some` values match this instance's values.
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool;
/// Returns a refinement that represents the difference between this instance and the given
/// refinement.
///
/// For refineable fields, this recursively calls `subtract`. For other fields, the field is
/// `None` if the field's value is equal to the refinement.
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement;
}
pub trait IsEmpty {
/// When `true`, indicates that use applying this refinement does nothing.
/// Returns `true` if applying this refinement would have no effect.
fn is_empty(&self) -> bool;
}
/// A cascade of refinements that can be merged in priority order.
///
/// A cascade maintains a sequence of optional refinements where later entries
/// take precedence over earlier ones. The first slot (index 0) is always the
/// base refinement and is guaranteed to be present.
///
/// This is useful for implementing configuration hierarchies like CSS cascading,
/// where styles from different sources (user agent, user, author) are combined
/// with specific precedence rules.
pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
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)]
pub struct CascadeSlot(usize);
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 {
self.0.push(None);
CascadeSlot(self.0.len() - 1)
}
/// Returns a mutable reference to the base refinement (slot 0).
///
/// The base refinement is always present and serves as the foundation
/// for the cascade.
pub fn base(&mut self) -> &mut S::Refinement {
self.0[0].as_mut().unwrap()
}
/// Sets the refinement for a specific slot in the cascade.
///
/// Setting a slot to `None` effectively removes it from consideration
/// during merging.
pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::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 {
let mut merged = self.0[0].clone().unwrap();
for refinement in self.0.iter().skip(1).flatten() {