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

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

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 test;
#[cfg(any(feature = "inspector", debug_assertions))]
mod derive_inspector_reflection;
use proc_macro::TokenStream;
use syn::{DeriveInput, Ident};
@ -178,6 +181,28 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
}
/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides
/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`.
/// This is used by the inspector so that it can use the builder methods in `Styled` and
/// `StyledExt`.
///
/// The generated module will have the name `<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> {
match &ast.data {
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"));
}