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