gpui_component_macros/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream};
4
5mod crate_path;
6mod derive_into_plot;
7
8struct IconNameInput {
10 enum_name: syn::Ident,
11 _comma: syn::Token![,],
12 path: syn::LitStr,
13 derives: Option<(
14 syn::Token![,],
15 syn::punctuated::Punctuated<syn::Path, syn::Token![,]>,
16 )>,
17}
18
19impl Parse for IconNameInput {
20 fn parse(input: ParseStream) -> syn::Result<Self> {
21 let enum_name = input.parse()?;
22 let _comma = input.parse()?;
23 let path = input.parse()?;
24
25 let derives = if input.peek(syn::Token![,]) {
27 let comma = input.parse()?;
28 let content;
29 syn::bracketed!(content in input);
30 let derives = content.parse_terminated(syn::Path::parse, syn::Token![,])?;
31 Some((comma, derives))
32 } else {
33 None
34 };
35
36 Ok(IconNameInput {
37 enum_name,
38 _comma,
39 path,
40 derives,
41 })
42 }
43}
44
45#[proc_macro_derive(IntoPlot)]
46pub fn derive_into_plot(input: TokenStream) -> TokenStream {
47 derive_into_plot::derive_into_plot(input)
48}
49
50fn pascal_case(filename: &str) -> String {
63 filename
64 .strip_suffix(".svg")
65 .unwrap_or(filename)
66 .split(|c: char| c == '-' || c == '_' || c == '.')
67 .filter(|part| !part.is_empty())
68 .map(|word| {
69 let mut chars = word.chars();
70 match chars.next() {
71 None => String::new(),
72 Some(first) if first.is_ascii_digit() => word.to_string(),
73 Some(first) => {
74 let mut result = String::with_capacity(word.len());
75 result.extend(first.to_uppercase());
76 result.push_str(&chars.as_str().to_lowercase());
77 result
78 }
79 }
80 })
81 .collect()
82}
83
84#[proc_macro]
112pub fn icon_named(input: TokenStream) -> TokenStream {
113 let IconNameInput {
114 enum_name,
115 path,
116 derives,
117 ..
118 } = syn::parse_macro_input!(input as IconNameInput);
119
120 let raw_path = path.value();
121
122 let icons_dir = if let Some(env_name) = raw_path.strip_prefix('$') {
128 let env_value = std::env::var(env_name).unwrap_or_else(|_| {
129 panic!(
130 "icon_named!: env var `{env_name}` is not set at expansion time. \
131 Ensure the calling crate's build.rs propagates it via \
132 `cargo:rustc-env={env_name}=<absolute path>`."
133 )
134 });
135 std::path::PathBuf::from(env_value)
136 } else {
137 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
138 std::path::Path::new(&manifest_dir).join(&raw_path)
139 };
140
141 let mut entries: Vec<(String, String)> = Vec::new();
142
143 let dir = std::fs::read_dir(&icons_dir).unwrap_or_else(|e| {
144 panic!(
145 "generate_icon_enum: failed to read '{}': {}",
146 icons_dir.display(),
147 e
148 )
149 });
150
151 for entry in dir {
152 let entry = entry.expect("failed to read directory entry");
153 let filename = entry.file_name().to_string_lossy().to_string();
154 if filename.ends_with(".svg") {
155 let variant_name = pascal_case(&filename);
156 let path = format!("icons/{}", filename);
157 entries.push((variant_name, path));
158 }
159 }
160
161 entries.sort_by(|a, b| a.0.cmp(&b.0));
162
163 let variants: Vec<proc_macro2::Ident> = entries
164 .iter()
165 .map(|(name, _)| proc_macro2::Ident::new(name, proc_macro2::Span::call_site()))
166 .collect();
167 let paths: Vec<&str> = entries.iter().map(|(_, p)| p.as_str()).collect();
168
169 let derive_attrs = if let Some((_, custom_derives)) = derives {
171 let derives_vec: Vec<_> = custom_derives.iter().collect();
172 quote! {
173 #[derive(IntoElement, Clone, #(#derives_vec),*)]
174 }
175 } else {
176 quote! {
177 #[derive(IntoElement, Clone)]
178 }
179 };
180
181 let expanded = quote! {
182 #derive_attrs
183
184 pub enum #enum_name {
185 #(#variants,)*
186 }
187
188 impl IconNamed for #enum_name {
189 fn path(self) -> SharedString {
190 match self {
191 #(Self::#variants => #paths,)*
192 }
193 .into()
194 }
195 }
196 };
197
198 TokenStream::from(expanded)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_pascal_case_basic() {
207 assert_eq!(pascal_case("arrow-right.svg"), "ArrowRight");
208 assert_eq!(pascal_case("home.svg"), "Home");
209 assert_eq!(pascal_case("x-circle.svg"), "XCircle");
210
211 assert_eq!(pascal_case("some_icon_name.svg"), "SomeIconName");
212 assert_eq!(pascal_case("arrow_up_down.svg"), "ArrowUpDown");
213
214 assert_eq!(pascal_case("kebab-case_mixed.svg"), "KebabCaseMixed");
215 assert_eq!(pascal_case("icon-with_under.svg"), "IconWithUnder");
216
217 assert_eq!(pascal_case("icon-123.svg"), "Icon123");
218 assert_eq!(pascal_case("arrow-2x.svg"), "Arrow2x");
219 assert_eq!(pascal_case("24-hour.svg"), "24Hour");
220
221 assert_eq!(pascal_case("arrow--right.svg"), "ArrowRight");
222 assert_eq!(pascal_case("icon__name.svg"), "IconName");
223 assert_eq!(pascal_case("multiple---dash.svg"), "MultipleDash");
224
225 assert_eq!(pascal_case("a.svg"), "A");
226 assert_eq!(pascal_case("-leading.svg"), "Leading");
227 assert_eq!(pascal_case("trailing-.svg"), "Trailing");
228 assert_eq!(pascal_case("-.svg"), "");
229
230 assert_eq!(pascal_case("arrow-right"), "ArrowRight");
231 assert_eq!(pascal_case("home"), "Home");
232
233 assert_eq!(pascal_case("hello.svg"), "Hello");
234 assert_eq!(pascal_case("WORLD.svg"), "World");
235 assert_eq!(pascal_case("iOS-icon.svg"), "IosIcon");
236 assert_eq!(pascal_case("API-key.svg"), "ApiKey");
237 }
238}