1use proc_macro::TokenStream;
25use quote::quote;
26use syn::{Data, DeriveInput, Expr, Fields, Lit, Meta, parse_macro_input};
27
28#[proc_macro_derive(Previewable, attributes(preview))]
29pub fn derive_previewable(input: TokenStream) -> TokenStream {
30 let input = parse_macro_input!(input as DeriveInput);
31
32 match &input.data {
33 Data::Struct(data) => derive_struct(&input, data),
34 Data::Enum(data) => derive_enum(&input, data),
35 Data::Union(_) => {
36 syn::Error::new_spanned(&input, "Previewable cannot be derived for unions")
37 .to_compile_error()
38 .into()
39 }
40 }
41}
42
43fn derive_struct(input: &DeriveInput, data: &syn::DataStruct) -> TokenStream {
46 let name = &input.ident;
47 let name_str = name.to_string();
48
49 let no_register = has_preview_flag(&input.attrs, "no_register");
50
51 let category =
52 extract_preview_kv(&input.attrs, "category").unwrap_or_else(|| "Uncategorized".to_string());
53 let description = extract_doc_comment(&input.attrs);
54
55 let Fields::Named(fields) = &data.fields else {
56 return syn::Error::new_spanned(&input.ident, "Previewable requires named fields")
57 .to_compile_error()
58 .into();
59 };
60
61 let mut field_metas = Vec::new();
62 let mut get_arms = Vec::new();
63 let mut set_arms = Vec::new();
64
65 for field in &fields.named {
66 let field_ident = field.ident.as_ref().unwrap();
67 let field_name = field_ident.to_string();
68
69 if has_preview_flag(&field.attrs, "skip") {
70 continue;
71 }
72
73 let doc = extract_doc_comment(&field.attrs);
74 let ty = &field.ty;
75
76 let control = if let Some((min, max)) = extract_slider_attr(&field.attrs) {
78 quote! { gpui_preview::ControlKind::NumberSlider { min: #min, max: #max } }
79 } else {
80 type_to_control(ty)
81 };
82
83 field_metas.push(quote! {
84 gpui_preview::FieldMeta {
85 name: #field_name,
86 doc: #doc,
87 control: #control,
88 }
89 });
90
91 get_arms.push(type_to_get(field_ident, &field_name, ty));
92 set_arms.push(type_to_set(field_ident, &field_name, ty));
93 }
94
95 let registration = if no_register {
96 quote! {}
97 } else {
98 quote! {
99 gpui_preview::inventory::submit! {
100 gpui_preview::PreviewEntry {
101 id: || std::any::type_name::<#name>(),
102 name: #name_str,
103 category: #category,
104 description: #description,
105 fields: <#name as gpui_preview::Previewable>::fields,
106 create_default: || {
107 Box::new(<#name as gpui_preview::Previewable>::default_preview())
108 },
109 render: |any: &dyn std::any::Any| -> gpui::AnyElement {
110 let instance = any.downcast_ref::<#name>().expect("type mismatch in render");
111 gpui::IntoElement::into_any_element(gpui::Component::new(instance.clone()))
112 },
113 }
114 }
115 }
116 };
117
118 let expanded = quote! {
119 impl gpui_preview::Previewable for #name {
120 fn name() -> &'static str { #name_str }
121 fn category() -> &'static str { #category }
122 fn description() -> &'static str { #description }
123
124 fn default_preview() -> Self {
125 Default::default()
126 }
127
128 fn fields() -> Vec<gpui_preview::FieldMeta> {
129 vec![#(#field_metas),*]
130 }
131
132 fn get_field(&self, name: &str) -> Option<gpui_preview::FieldValue> {
133 match name {
134 #(#get_arms)*
135 _ => None,
136 }
137 }
138
139 fn set_field(&self, name: &str, value: gpui_preview::FieldValue) -> Self {
140 let mut new = self.clone();
141 match (name, value) {
142 #(#set_arms)*
143 _ => {}
144 }
145 new
146 }
147 }
148
149 #registration
150 };
151
152 expanded.into()
153}
154
155fn derive_enum(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
158 let name = &input.ident;
159
160 let mut variant_idents = Vec::new();
161 let mut variant_strs = Vec::new();
162
163 for variant in &data.variants {
164 if !variant.fields.is_empty() {
165 return syn::Error::new_spanned(
166 variant,
167 "Previewable enums must have only unit variants (no fields)",
168 )
169 .to_compile_error()
170 .into();
171 }
172 variant_idents.push(&variant.ident);
173 variant_strs.push(variant.ident.to_string());
174 }
175
176 let expanded = quote! {
177 impl gpui_preview::PreviewEnum for #name {
178 fn variants() -> &'static [&'static str] {
179 &[#(#variant_strs),*]
180 }
181
182 fn to_variant_name(&self) -> &'static str {
183 match self {
184 #(Self::#variant_idents => #variant_strs,)*
185 }
186 }
187
188 fn from_variant_name(name: &str) -> Option<Self> {
189 match name {
190 #(#variant_strs => Some(Self::#variant_idents),)*
191 _ => None,
192 }
193 }
194 }
195 };
196
197 expanded.into()
198}
199
200fn type_to_control(ty: &syn::Type) -> proc_macro2::TokenStream {
203 if let Some(inner) = extract_option_inner(ty) {
204 let inner_control = type_to_control(inner);
205 return quote! { gpui_preview::ControlKind::Optional(Box::new(#inner_control)) };
206 }
207
208 let Some(type_name) = last_segment_name(ty) else {
209 return quote! { gpui_preview::ControlKind::Unsupported };
210 };
211
212 match type_name.as_str() {
213 "String" => quote! { gpui_preview::ControlKind::TextInput },
214 "bool" => quote! { gpui_preview::ControlKind::Toggle },
215 "f32" | "f64" => {
216 quote! { gpui_preview::ControlKind::NumberSlider { min: 0.0, max: 100.0 } }
217 }
218 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize" => {
219 quote! { gpui_preview::ControlKind::NumberSlider { min: 0.0, max: 100.0 } }
220 }
221 "Hsla" => quote! { gpui_preview::ControlKind::Color },
222 _ => {
223 quote! {
225 gpui_preview::ControlKind::Select(
226 <#ty as gpui_preview::PreviewEnum>::variants().to_vec()
227 )
228 }
229 }
230 }
231}
232
233fn type_to_get(
236 field_ident: &syn::Ident,
237 field_name: &str,
238 ty: &syn::Type,
239) -> proc_macro2::TokenStream {
240 if let Some(inner) = extract_option_inner(ty) {
241 let inner_get = type_to_get_expr(field_ident, inner, true);
242 return quote! {
243 #field_name => match &self.#field_ident {
244 Some(_opt_inner) => #inner_get,
245 None => Some(gpui_preview::FieldValue::None),
246 },
247 };
248 }
249
250 let Some(type_name) = last_segment_name(ty) else {
251 return quote! { #field_name => None, };
252 };
253
254 match type_name.as_str() {
255 "String" => quote! {
256 #field_name => Some(gpui_preview::FieldValue::String(self.#field_ident.clone())),
257 },
258 "bool" => quote! {
259 #field_name => Some(gpui_preview::FieldValue::Bool(self.#field_ident)),
260 },
261 "f32" | "f64" => quote! {
262 #field_name => Some(gpui_preview::FieldValue::Float(self.#field_ident as f64)),
263 },
264 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
265 quote! {
266 #field_name => Some(gpui_preview::FieldValue::Int(self.#field_ident as i64)),
267 }
268 }
269 "Hsla" => quote! {
270 #field_name => {
271 let rgba: gpui::Rgba = self.#field_ident.into();
272 Some(gpui_preview::FieldValue::Color([
273 (rgba.r * 255.0) as u8,
274 (rgba.g * 255.0) as u8,
275 (rgba.b * 255.0) as u8,
276 (rgba.a * 255.0) as u8,
277 ]))
278 },
279 },
280 _ => quote! {
281 #field_name => Some(gpui_preview::FieldValue::Enum(
282 gpui_preview::PreviewEnum::to_variant_name(&self.#field_ident).to_string()
283 )),
284 },
285 }
286}
287
288fn type_to_get_expr(
289 field_ident: &syn::Ident,
290 ty: &syn::Type,
291 is_option_inner: bool,
292) -> proc_macro2::TokenStream {
293 let src = if is_option_inner {
294 quote! { _opt_inner }
295 } else {
296 quote! { self.#field_ident }
297 };
298
299 let Some(type_name) = last_segment_name(ty) else {
300 return quote! { None };
301 };
302
303 match type_name.as_str() {
304 "String" => quote! { Some(gpui_preview::FieldValue::String(#src.clone())) },
305 "bool" => quote! { Some(gpui_preview::FieldValue::Bool(*#src)) },
306 "f32" | "f64" => quote! { Some(gpui_preview::FieldValue::Float(*#src as f64)) },
307 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
308 quote! { Some(gpui_preview::FieldValue::Int(*#src as i64)) }
309 }
310 "Hsla" => quote! {
311 {
312 let rgba: gpui::Rgba = (*#src).into();
313 Some(gpui_preview::FieldValue::Color([
314 (rgba.r * 255.0) as u8,
315 (rgba.g * 255.0) as u8,
316 (rgba.b * 255.0) as u8,
317 (rgba.a * 255.0) as u8,
318 ]))
319 }
320 },
321 _ => quote! {
322 Some(gpui_preview::FieldValue::Enum(
323 gpui_preview::PreviewEnum::to_variant_name(#src).to_string()
324 ))
325 },
326 }
327}
328
329fn type_to_set(
332 field_ident: &syn::Ident,
333 field_name: &str,
334 ty: &syn::Type,
335) -> proc_macro2::TokenStream {
336 if let Some(inner) = extract_option_inner(ty) {
337 let inner_set = type_to_set_expr(field_ident, inner, true);
338 return quote! {
339 (#field_name, gpui_preview::FieldValue::None) => { new.#field_ident = None; }
340 #inner_set
341 };
342 }
343
344 let Some(type_name) = last_segment_name(ty) else {
345 return quote! {};
346 };
347
348 match type_name.as_str() {
349 "String" => quote! {
350 (#field_name, gpui_preview::FieldValue::String(v)) => { new.#field_ident = v; }
351 },
352 "bool" => quote! {
353 (#field_name, gpui_preview::FieldValue::Bool(v)) => { new.#field_ident = v; }
354 },
355 "f32" => quote! {
356 (#field_name, gpui_preview::FieldValue::Float(v)) => { new.#field_ident = v as f32; }
357 },
358 "f64" => quote! {
359 (#field_name, gpui_preview::FieldValue::Float(v)) => { new.#field_ident = v; }
360 },
361 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
362 quote! {
363 (#field_name, gpui_preview::FieldValue::Int(v)) => { new.#field_ident = v as #ty; }
364 }
365 }
366 "Hsla" => quote! {
367 (#field_name, gpui_preview::FieldValue::Color(v)) => {
368 new.#field_ident = gpui::Hsla::from(gpui::Rgba {
369 r: v[0] as f32 / 255.0,
370 g: v[1] as f32 / 255.0,
371 b: v[2] as f32 / 255.0,
372 a: v[3] as f32 / 255.0,
373 });
374 }
375 },
376 _ => quote! {
377 (#field_name, gpui_preview::FieldValue::Enum(ref v)) => {
378 if let Some(e) = <#ty as gpui_preview::PreviewEnum>::from_variant_name(v) {
379 new.#field_ident = e;
380 }
381 }
382 },
383 }
384}
385
386fn type_to_set_expr(
387 field_ident: &syn::Ident,
388 inner_ty: &syn::Type,
389 _is_option: bool,
390) -> proc_macro2::TokenStream {
391 let Some(type_name) = last_segment_name(inner_ty) else {
392 return quote! {};
393 };
394
395 let field_name = field_ident.to_string();
396
397 match type_name.as_str() {
398 "String" => quote! {
399 (#field_name, gpui_preview::FieldValue::String(v)) => { new.#field_ident = Some(v); }
400 },
401 "bool" => quote! {
402 (#field_name, gpui_preview::FieldValue::Bool(v)) => { new.#field_ident = Some(v); }
403 },
404 "f32" => quote! {
405 (#field_name, gpui_preview::FieldValue::Float(v)) => { new.#field_ident = Some(v as f32); }
406 },
407 "f64" => quote! {
408 (#field_name, gpui_preview::FieldValue::Float(v)) => { new.#field_ident = Some(v); }
409 },
410 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
411 quote! {
412 (#field_name, gpui_preview::FieldValue::Int(v)) => { new.#field_ident = Some(v as #inner_ty); }
413 }
414 }
415 "Hsla" => quote! {
416 (#field_name, gpui_preview::FieldValue::Color(v)) => {
417 new.#field_ident = Some(gpui::Hsla::from(gpui::Rgba {
418 r: v[0] as f32 / 255.0,
419 g: v[1] as f32 / 255.0,
420 b: v[2] as f32 / 255.0,
421 a: v[3] as f32 / 255.0,
422 }));
423 }
424 },
425 _ => quote! {
426 (#field_name, gpui_preview::FieldValue::Enum(ref v)) => {
427 if let Some(e) = <#inner_ty as gpui_preview::PreviewEnum>::from_variant_name(v) {
428 new.#field_ident = Some(e);
429 }
430 }
431 },
432 }
433}
434
435fn extract_preview_kv(attrs: &[syn::Attribute], key: &str) -> Option<String> {
438 for attr in attrs {
439 if !attr.path().is_ident("preview") {
440 continue;
441 }
442 let Ok(nested) = attr
443 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
444 else {
445 continue;
446 };
447 for meta in &nested {
448 if let Meta::NameValue(nv) = meta
449 && nv.path.is_ident(key)
450 && let Expr::Lit(expr_lit) = &nv.value
451 && let Lit::Str(lit_str) = &expr_lit.lit
452 {
453 return Some(lit_str.value());
454 }
455 }
456 }
457 None
458}
459
460fn has_preview_flag(attrs: &[syn::Attribute], flag: &str) -> bool {
461 for attr in attrs {
462 if !attr.path().is_ident("preview") {
463 continue;
464 }
465 let Ok(nested) = attr
466 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
467 else {
468 continue;
469 };
470 for meta in &nested {
471 if let Meta::Path(path) = meta
472 && path.is_ident(flag)
473 {
474 return true;
475 }
476 }
477 }
478 false
479}
480
481fn extract_slider_attr(attrs: &[syn::Attribute]) -> Option<(f64, f64)> {
482 for attr in attrs {
483 if !attr.path().is_ident("preview") {
484 continue;
485 }
486 let Ok(nested) = attr
487 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
488 else {
489 continue;
490 };
491 for meta in &nested {
492 if let Meta::List(list) = meta
493 && list.path.is_ident("slider")
494 {
495 let Ok(inner) = list.parse_args_with(
496 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
497 ) else {
498 continue;
499 };
500 let mut min = 0.0f64;
501 let mut max = 100.0f64;
502 for m in &inner {
503 if let Meta::NameValue(nv) = m
504 && let Expr::Lit(expr_lit) = &nv.value
505 && let Lit::Float(f) = &expr_lit.lit
506 {
507 let val: f64 = f.base10_parse().unwrap();
508 if nv.path.is_ident("min") {
509 min = val;
510 } else if nv.path.is_ident("max") {
511 max = val;
512 }
513 }
514 }
515 return Some((min, max));
516 }
517 }
518 }
519 None
520}
521
522fn extract_doc_comment(attrs: &[syn::Attribute]) -> String {
523 let mut lines = Vec::new();
524 for attr in attrs {
525 if !attr.path().is_ident("doc") {
526 continue;
527 }
528 if let Meta::NameValue(nv) = &attr.meta
529 && let Expr::Lit(expr_lit) = &nv.value
530 && let Lit::Str(lit_str) = &expr_lit.lit
531 {
532 lines.push(lit_str.value().trim().to_string());
533 }
534 }
535 lines.join(" ")
536}
537
538fn last_segment_name(ty: &syn::Type) -> Option<String> {
539 if let syn::Type::Path(type_path) = ty {
540 type_path.path.segments.last().map(|s| s.ident.to_string())
541 } else {
542 None
543 }
544}
545
546fn extract_option_inner(ty: &syn::Type) -> Option<&syn::Type> {
547 let syn::Type::Path(type_path) = ty else {
548 return None;
549 };
550 let segment = type_path.path.segments.last()?;
551 if segment.ident != "Option" {
552 return None;
553 }
554 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
555 return None;
556 };
557 let syn::GenericArgument::Type(inner) = args.args.first()? else {
558 return None;
559 };
560 Some(inner)
561}