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 let line = lit_str.value();
168 let line = line.strip_prefix(' ').unwrap_or(&line);
169 doc_lines.push(line.to_string());
170 }
171 }
172
173 if doc_lines.is_empty() {
174 None
175 } else {
176 Some(doc_lines.join("\n"))
177 }
178}
179
180fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
181 attrs
182 .iter()
183 .filter(|attr| attr.path().is_ident("cfg"))
184 .cloned()
185 .collect()
186}
187
188fn is_called_from_gpui_crate(_span: Span) -> bool {
189 // Check if we're being called from within the gpui crate by examining the call site
190 // This is a heuristic approach - we check if the current crate name is "gpui"
191 std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui")
192}
193
194struct MacroExpander;
195
196impl VisitMut for MacroExpander {
197 fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) {
198 let mut expanded_items = Vec::new();
199 let mut items_to_keep = Vec::new();
200
201 for item in trait_item.items.drain(..) {
202 match item {
203 TraitItem::Macro(macro_item) => {
204 // Try to expand known macros
205 if let Some(expanded) = try_expand_macro(¯o_item) {
206 expanded_items.extend(expanded);
207 } else {
208 // Keep unknown macros as-is
209 items_to_keep.push(TraitItem::Macro(macro_item));
210 }
211 }
212 other => {
213 items_to_keep.push(other);
214 }
215 }
216 }
217
218 // Rebuild the items list with expanded content first, then original items
219 trait_item.items = expanded_items;
220 trait_item.items.extend(items_to_keep);
221
222 // Continue visiting
223 visit_mut::visit_item_trait_mut(self, trait_item);
224 }
225}
226
227fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
228 let path = ¯o_item.mac.path;
229
230 // Check if this is one of our known style macros
231 let macro_name = path_to_string(path);
232
233 // Handle the known macros by calling their implementations
234 match macro_name.as_str() {
235 "gpui_macros::style_helpers" | "style_helpers" => {
236 let tokens = macro_item.mac.tokens.clone();
237 let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
238 parse_expanded_items(expanded)
239 }
240 "gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
241 let tokens = macro_item.mac.tokens.clone();
242 let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
243 parse_expanded_items(expanded)
244 }
245 "gpui_macros::margin_style_methods" | "margin_style_methods" => {
246 let tokens = macro_item.mac.tokens.clone();
247 let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
248 parse_expanded_items(expanded)
249 }
250 "gpui_macros::padding_style_methods" | "padding_style_methods" => {
251 let tokens = macro_item.mac.tokens.clone();
252 let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
253 parse_expanded_items(expanded)
254 }
255 "gpui_macros::position_style_methods" | "position_style_methods" => {
256 let tokens = macro_item.mac.tokens.clone();
257 let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
258 parse_expanded_items(expanded)
259 }
260 "gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
261 let tokens = macro_item.mac.tokens.clone();
262 let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
263 parse_expanded_items(expanded)
264 }
265 "gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
266 let tokens = macro_item.mac.tokens.clone();
267 let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
268 parse_expanded_items(expanded)
269 }
270 "gpui_macros::border_style_methods" | "border_style_methods" => {
271 let tokens = macro_item.mac.tokens.clone();
272 let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
273 parse_expanded_items(expanded)
274 }
275 "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
276 let tokens = macro_item.mac.tokens.clone();
277 let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
278 parse_expanded_items(expanded)
279 }
280 _ => None,
281 }
282}
283
284fn path_to_string(path: &Path) -> String {
285 path.segments
286 .iter()
287 .map(|seg| seg.ident.to_string())
288 .collect::<Vec<_>>()
289 .join("::")
290}
291
292fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
293 let tokens = TokenStream2::from(expanded);
294
295 // Try to parse the expanded tokens as trait items
296 // We need to wrap them in a dummy trait to parse properly
297 let dummy_trait: ItemTrait = parse_quote! {
298 trait Dummy {
299 #tokens
300 }
301 };
302
303 Some(dummy_trait.items)
304}