1//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods
2//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4.
3
4use heck::ToSnakeCase as _;
5use proc_macro::TokenStream;
6use proc_macro2::{Span, TokenStream as TokenStream2};
7use quote::quote;
8use syn::{
9 Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type,
10 parse_macro_input, parse_quote,
11 visit_mut::{self, VisitMut},
12};
13
14pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
15 let mut item = parse_macro_input!(input as Item);
16
17 // First, expand any macros in the trait
18 match &mut item {
19 Item::Trait(trait_item) => {
20 let mut expander = MacroExpander;
21 expander.visit_item_trait_mut(trait_item);
22 }
23 _ => {
24 return syn::Error::new_spanned(
25 quote!(#item),
26 "#[derive_inspector_reflection] can only be applied to traits",
27 )
28 .to_compile_error()
29 .into();
30 }
31 }
32
33 // Now process the expanded trait
34 match item {
35 Item::Trait(trait_item) => generate_reflected_trait(trait_item),
36 _ => unreachable!(),
37 }
38}
39
40fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream {
41 let trait_name = &trait_item.ident;
42 let vis = &trait_item.vis;
43
44 // Determine if we're being called from within the gpui crate
45 let call_site = Span::call_site();
46 let inspector_reflection_path = if is_called_from_gpui_crate(call_site) {
47 quote! { crate::inspector_reflection }
48 } else {
49 quote! { ::gpui::inspector_reflection }
50 };
51
52 // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self
53 let mut method_infos = Vec::new();
54
55 for item in &trait_item.items {
56 if let TraitItem::Fn(method) = item {
57 let method_name = &method.sig.ident;
58
59 // Check if method has self or mut self receiver
60 let has_valid_self_receiver = method
61 .sig
62 .inputs
63 .iter()
64 .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none()));
65
66 // Check if method returns Self
67 let returns_self = match &method.sig.output {
68 ReturnType::Type(_, ty) => {
69 matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self"))
70 }
71 ReturnType::Default => false,
72 };
73
74 // Check if method has exactly one parameter (self or mut self)
75 let param_count = method.sig.inputs.len();
76
77 // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self
78 // This includes methods with default implementations
79 if has_valid_self_receiver && returns_self && param_count == 1 {
80 // Extract documentation and cfg attributes
81 let doc = extract_doc_comment(&method.attrs);
82 let cfg_attrs = extract_cfg_attributes(&method.attrs);
83 method_infos.push((method_name.clone(), doc, cfg_attrs));
84 }
85 }
86 }
87
88 // Generate the reflection module name
89 let reflection_mod_name = Ident::new(
90 &format!("{}_reflection", trait_name.to_string().to_snake_case()),
91 trait_name.span(),
92 );
93
94 // Generate wrapper functions for each method
95 // These wrappers use type erasure to allow runtime invocation
96 let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| {
97 let wrapper_name = Ident::new(
98 &format!("__wrapper_{}", method_name),
99 method_name.span(),
100 );
101 quote! {
102 #(#cfg_attrs)*
103 fn #wrapper_name<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
104 if let Ok(concrete) = value.downcast::<T>() {
105 Box::new(concrete.#method_name())
106 } else {
107 panic!("Type mismatch in reflection wrapper");
108 }
109 }
110 }
111 });
112
113 // Generate method info entries
114 let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| {
115 let method_name_str = method_name.to_string();
116 let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span());
117 let doc_expr = match doc {
118 Some(doc_str) => quote! { Some(#doc_str) },
119 None => quote! { None },
120 };
121 quote! {
122 #(#cfg_attrs)*
123 #inspector_reflection_path::FunctionReflection {
124 name: #method_name_str,
125 function: #wrapper_name::<T>,
126 documentation: #doc_expr,
127 _type: ::std::marker::PhantomData,
128 }
129 }
130 });
131
132 // Generate the complete output
133 let output = quote! {
134 #trait_item
135
136 /// Implements function reflection
137 #vis mod #reflection_mod_name {
138 use super::*;
139
140 #(#wrapper_functions)*
141
142 /// Get all reflectable methods for a concrete type implementing the trait
143 pub fn methods<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
144 vec![
145 #(#method_info_entries),*
146 ]
147 }
148
149 /// Find a method by name for a concrete type implementing the trait
150 pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
151 methods::<T>().into_iter().find(|m| m.name == name)
152 }
153 }
154 };
155
156 TokenStream::from(output)
157}
158
159fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
160 let mut doc_lines = Vec::new();
161
162 for attr in attrs {
163 if attr.path().is_ident("doc")
164 && let Meta::NameValue(meta) = &attr.meta
165 && let Expr::Lit(expr_lit) = &meta.value
166 && let Lit::Str(lit_str) = &expr_lit.lit
167 {
168 let line = lit_str.value();
169 let line = line.strip_prefix(' ').unwrap_or(&line);
170 doc_lines.push(line.to_string());
171 }
172 }
173
174 if doc_lines.is_empty() {
175 None
176 } else {
177 Some(doc_lines.join("\n"))
178 }
179}
180
181fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
182 attrs
183 .iter()
184 .filter(|attr| attr.path().is_ident("cfg"))
185 .cloned()
186 .collect()
187}
188
189fn is_called_from_gpui_crate(_span: Span) -> bool {
190 // Check if we're being called from within the gpui crate by examining the call site
191 // This is a heuristic approach - we check if the current crate name is "gpui"
192 std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui")
193}
194
195struct MacroExpander;
196
197impl VisitMut for MacroExpander {
198 fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) {
199 let mut expanded_items = Vec::new();
200 let mut items_to_keep = Vec::new();
201
202 for item in trait_item.items.drain(..) {
203 match item {
204 TraitItem::Macro(macro_item) => {
205 // Try to expand known macros
206 if let Some(expanded) = try_expand_macro(¯o_item) {
207 expanded_items.extend(expanded);
208 } else {
209 // Keep unknown macros as-is
210 items_to_keep.push(TraitItem::Macro(macro_item));
211 }
212 }
213 other => {
214 items_to_keep.push(other);
215 }
216 }
217 }
218
219 // Rebuild the items list with expanded content first, then original items
220 trait_item.items = expanded_items;
221 trait_item.items.extend(items_to_keep);
222
223 // Continue visiting
224 visit_mut::visit_item_trait_mut(self, trait_item);
225 }
226}
227
228fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
229 let path = ¯o_item.mac.path;
230
231 // Check if this is one of our known style macros
232 let macro_name = path_to_string(path);
233
234 // Handle the known macros by calling their implementations
235 match macro_name.as_str() {
236 "gpui_macros::style_helpers" | "style_helpers" => {
237 let tokens = macro_item.mac.tokens.clone();
238 let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
239 parse_expanded_items(expanded)
240 }
241 "gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
242 let tokens = macro_item.mac.tokens.clone();
243 let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
244 parse_expanded_items(expanded)
245 }
246 "gpui_macros::margin_style_methods" | "margin_style_methods" => {
247 let tokens = macro_item.mac.tokens.clone();
248 let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
249 parse_expanded_items(expanded)
250 }
251 "gpui_macros::padding_style_methods" | "padding_style_methods" => {
252 let tokens = macro_item.mac.tokens.clone();
253 let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
254 parse_expanded_items(expanded)
255 }
256 "gpui_macros::position_style_methods" | "position_style_methods" => {
257 let tokens = macro_item.mac.tokens.clone();
258 let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
259 parse_expanded_items(expanded)
260 }
261 "gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
262 let tokens = macro_item.mac.tokens.clone();
263 let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
264 parse_expanded_items(expanded)
265 }
266 "gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
267 let tokens = macro_item.mac.tokens.clone();
268 let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
269 parse_expanded_items(expanded)
270 }
271 "gpui_macros::border_style_methods" | "border_style_methods" => {
272 let tokens = macro_item.mac.tokens.clone();
273 let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
274 parse_expanded_items(expanded)
275 }
276 "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
277 let tokens = macro_item.mac.tokens.clone();
278 let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
279 parse_expanded_items(expanded)
280 }
281 _ => None,
282 }
283}
284
285fn path_to_string(path: &Path) -> String {
286 path.segments
287 .iter()
288 .map(|seg| seg.ident.to_string())
289 .collect::<Vec<_>>()
290 .join("::")
291}
292
293fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
294 let tokens = TokenStream2::from(expanded);
295
296 // Try to parse the expanded tokens as trait items
297 // We need to wrap them in a dummy trait to parse properly
298 let dummy_trait: ItemTrait = parse_quote! {
299 trait Dummy {
300 #tokens
301 }
302 };
303
304 Some(dummy_trait.items)
305}