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:
parent
6253b95f82
commit
649072d140
35 changed files with 1778 additions and 316 deletions
|
@ -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"] }
|
||||
|
|
307
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal file
307
crates/gpui_macros/src/derive_inspector_reflection.rs
Normal 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(¯o_item) {
|
||||
expanded_items.extend(expanded);
|
||||
} else {
|
||||
// Keep unknown macros as-is
|
||||
items_to_keep.push(TraitItem::Macro(macro_item));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
items_to_keep.push(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the items list with expanded content first, then original items
|
||||
trait_item.items = expanded_items;
|
||||
trait_item.items.extend(items_to_keep);
|
||||
|
||||
// Continue visiting
|
||||
visit_mut::visit_item_trait_mut(self, trait_item);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
|
||||
let path = ¯o_item.mac.path;
|
||||
|
||||
// Check if this is one of our known style macros
|
||||
let macro_name = path_to_string(path);
|
||||
|
||||
// Handle the known macros by calling their implementations
|
||||
match macro_name.as_str() {
|
||||
"gpui_macros::style_helpers" | "style_helpers" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::margin_style_methods" | "margin_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::padding_style_methods" | "padding_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::position_style_methods" | "position_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::border_style_methods" | "border_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
"gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
|
||||
let tokens = macro_item.mac.tokens.clone();
|
||||
let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
|
||||
parse_expanded_items(expanded)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_string(path: &Path) -> String {
|
||||
path.segments
|
||||
.iter()
|
||||
.map(|seg| seg.ident.to_string())
|
||||
.collect::<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)
|
||||
}
|
|
@ -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
|
||||
|
|
148
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal file
148
crates/gpui_macros/tests/derive_inspector_reflection.rs
Normal 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"));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue