1use proc_macro::TokenStream;
38use proc_macro2::TokenStream as TokenStream2;
39use quote::{format_ident, quote};
40use syn::{
41 Attribute, Expr, ExprLit, Ident, Lit, LitStr, Path, Token, Type,
42 parse::{Parse, ParseStream},
43 parse_macro_input,
44 punctuated::Punctuated,
45};
46
47#[proc_macro]
55pub fn options(input: TokenStream) -> TokenStream {
56 let parsed = parse_macro_input!(input as OptionsInput);
57 match expand_options(&parsed) {
58 Ok(ts) => ts.into(),
59 Err(e) => e.to_compile_error().into(),
60 }
61}
62
63struct OptionsInput {
64 namespace: Option<LitStr>,
65 group: Path,
66 decls: Vec<OptionDecl>,
67}
68
69struct OptionDecl {
70 attrs: Vec<Attribute>,
71 name: Ident,
72 ty: Type,
73 default: Expr,
74}
75
76impl Parse for OptionsInput {
77 fn parse(input: ParseStream) -> syn::Result<Self> {
78 let namespace = if peek_keyword(input, "namespace") {
80 let _: Ident = input.parse()?;
81 let _: Token![=] = input.parse()?;
82 let s: LitStr = input.parse()?;
83 let _: Token![;] = input.parse()?;
84 Some(s)
85 } else {
86 None
87 };
88
89 let group_kw: Ident = input.parse()?;
91 if group_kw != "group" {
92 return Err(syn::Error::new(
93 group_kw.span(),
94 "expected `group = <path>;` directive",
95 ));
96 }
97 let _: Token![=] = input.parse()?;
98 let group: Path = input.parse()?;
99 let _: Token![;] = input.parse()?;
100
101 let mut decls = Vec::new();
103 while !input.is_empty() {
104 let attrs = input.call(Attribute::parse_outer)?;
105 let _: Token![pub] = input.parse()?;
106 let name: Ident = input.parse()?;
107 let _: Token![:] = input.parse()?;
108 let ty: Type = input.parse()?;
109 let _: Token![=] = input.parse()?;
110 let default: Expr = input.parse()?;
111 let _: Token![;] = input.parse()?;
112 decls.push(OptionDecl {
113 attrs,
114 name,
115 ty,
116 default,
117 });
118 }
119
120 Ok(Self {
121 namespace,
122 group,
123 decls,
124 })
125 }
126}
127
128fn peek_keyword(input: ParseStream, name: &str) -> bool {
129 let fork = input.fork();
130 if let Ok(id) = fork.parse::<Ident>() {
131 id == name
132 } else {
133 false
134 }
135}
136
137fn expand_options(input: &OptionsInput) -> syn::Result<TokenStream2> {
138 let mut out = TokenStream2::new();
139 for decl in &input.decls {
140 out.extend(expand_one_option(
141 decl,
142 &input.group,
143 input.namespace.as_ref(),
144 )?);
145 }
146 Ok(out)
147}
148
149fn expand_one_option(
150 decl: &OptionDecl,
151 group: &Path,
152 namespace: Option<&LitStr>,
153) -> syn::Result<TokenStream2> {
154 let name_ident = &decl.name;
155 let ty = &decl.ty;
156 let default = &decl.default;
157
158 let facts = OptionFacts::extract(&decl.attrs)?;
160
161 let display_name = match &facts.explicit_name {
163 Some(s) => s.value(),
164 None => {
165 let kebab = camel_to_kebab(&name_ident.to_string());
166 match namespace {
167 Some(ns) => format!("{}.{}", ns.value(), kebab),
168 None => kebab,
169 }
170 }
171 };
172
173 let doc = facts.doc;
174 let aliases = &facts.aliases;
175 let validate = facts.validate.as_ref();
176 let customizable = facts.customizable;
177
178 let aliases_lit: Vec<LitStr> = aliases
184 .iter()
185 .map(|s| LitStr::new(&s.value(), s.span()))
186 .collect();
187 let aliases_slice = if aliases_lit.is_empty() {
188 quote! { &[] as &[&'static str] }
189 } else {
190 quote! { &[#(#aliases_lit),*] }
191 };
192 let validate_chain = match validate {
193 Some(path) => quote! { .validate(#path) },
194 None => quote! {},
195 };
196
197 let display_name_lit = LitStr::new(&display_name, name_ident.span());
198 let doc_lit = LitStr::new(&doc, name_ident.span());
199
200 let link_name = format_ident!(
206 "__LATTICE_OPTION_DECL_{}",
207 uppercase(&name_ident.to_string())
208 );
209
210 Ok(quote! {
211 #[doc = #doc_lit]
213 pub struct #name_ident;
214
215 impl ::lattice_config::OptionDecl for #name_ident {
216 type Value = #ty;
217 const NAME: &'static str = #display_name_lit;
218 const DOC: &'static str = #doc_lit;
219 const CUSTOMIZABLE: bool = #customizable;
220 fn default_value() -> Self::Value {
221 #default
222 }
223 }
224
225 impl ::lattice_config::HasGroup for #name_ident {
226 const GROUP_NAME: &'static str =
227 <#group as ::lattice_config::OptionGroup>::NAME;
228 }
229
230 impl #name_ident {
231 pub fn build_spec() -> ::lattice_config::option::Option<#ty> {
240 let b = ::lattice_config::option::Option::<#ty>::builder(
241 #display_name_lit,
242 #default,
243 #doc_lit,
244 );
245 #[allow(clippy::let_and_return)]
246 let b = b.aliases(#aliases_slice);
247 let b = b #validate_chain;
248 b.build()
249 }
250 }
251
252 const _: () = {
260 #[::lattice_config::linkme::distributed_slice(
261 ::lattice_config::OPTION_DECLS
262 )]
263 #[linkme(crate = ::lattice_config::linkme)]
264 static #link_name: &::lattice_config::OptionDeclMetadata =
265 &::lattice_config::OptionDeclMetadata::for_decl::<#name_ident>(
266 <#ty as ::lattice_config::OptionType>::type_label,
267 || <#ty as ::lattice_config::OptionType>::format(&#default),
268 |registry: &::lattice_config::ConfigRegistry| {
269 let _ = registry.register_with_typeid::<#ty>(
270 <#name_ident>::build_spec(),
271 ::std::any::TypeId::of::<#name_ident>(),
272 );
273 },
274 );
275 };
276 })
277}
278
279#[derive(Default)]
280struct OptionFacts {
281 doc: String,
282 aliases: Vec<LitStr>,
283 validate: Option<Path>,
284 explicit_name: Option<LitStr>,
285 customizable: bool,
286}
287
288impl OptionFacts {
289 fn extract(attrs: &[Attribute]) -> syn::Result<Self> {
290 let mut facts = Self {
291 customizable: true,
292 ..Self::default()
293 };
294 let mut doc_lines: Vec<String> = Vec::new();
295 for attr in attrs {
296 if attr.path().is_ident("doc") {
297 if let syn::Meta::NameValue(nv) = &attr.meta
298 && let Expr::Lit(ExprLit {
299 lit: Lit::Str(s), ..
300 }) = &nv.value
301 {
302 let line = s.value();
303 let trimmed = line.strip_prefix(' ').unwrap_or(&line);
304 doc_lines.push(trimmed.to_string());
305 }
306 } else if attr.path().is_ident("aliases") {
307 let parsed: Punctuated<LitStr, Token![,]> =
308 attr.parse_args_with(Punctuated::parse_terminated)?;
309 for s in parsed {
310 facts.aliases.push(s);
311 }
312 } else if attr.path().is_ident("validate") {
313 let p: Path = attr.parse_args()?;
314 facts.validate = Some(p);
315 } else if attr.path().is_ident("name") {
316 let s: LitStr = attr.parse_args()?;
317 facts.explicit_name = Some(s);
318 } else if attr.path().is_ident("customizable") {
319 let b: syn::LitBool = attr.parse_args()?;
320 facts.customizable = b.value;
321 }
322 }
325 facts.doc = doc_lines.join("\n");
326 Ok(facts)
327 }
328}
329
330#[proc_macro]
338pub fn groups(input: TokenStream) -> TokenStream {
339 let parsed = parse_macro_input!(input as GroupsInput);
340 match expand_groups(&parsed) {
341 Ok(ts) => ts.into(),
342 Err(e) => e.to_compile_error().into(),
343 }
344}
345
346struct GroupsInput {
347 decls: Vec<GroupDecl>,
348}
349
350struct GroupDecl {
351 attrs: Vec<Attribute>,
352 name: Ident,
353 explicit_name: Option<LitStr>,
354}
355
356impl Parse for GroupsInput {
357 fn parse(input: ParseStream) -> syn::Result<Self> {
358 let mut decls = Vec::new();
359 while !input.is_empty() {
360 let attrs = input.call(Attribute::parse_outer)?;
361 let _: Token![pub] = input.parse()?;
362 let name: Ident = input.parse()?;
363 let explicit_name = if input.peek(Token![=]) {
364 let _: Token![=] = input.parse()?;
365 Some(input.parse::<LitStr>()?)
366 } else {
367 None
368 };
369 let _: Token![;] = input.parse()?;
370 decls.push(GroupDecl {
371 attrs,
372 name,
373 explicit_name,
374 });
375 }
376 Ok(Self { decls })
377 }
378}
379
380fn expand_groups(input: &GroupsInput) -> syn::Result<TokenStream2> {
381 let mut out = TokenStream2::new();
382 for decl in &input.decls {
383 out.extend(expand_one_group(decl)?);
384 }
385 Ok(out)
386}
387
388fn expand_one_group(decl: &GroupDecl) -> syn::Result<TokenStream2> {
389 let name_ident = &decl.name;
390 let display_name = match &decl.explicit_name {
391 Some(s) => s.value(),
392 None => camel_to_kebab(&name_ident.to_string()),
393 };
394
395 let mut doc_lines: Vec<String> = Vec::new();
397 for attr in &decl.attrs {
398 if attr.path().is_ident("doc")
399 && let syn::Meta::NameValue(nv) = &attr.meta
400 && let Expr::Lit(ExprLit {
401 lit: Lit::Str(s), ..
402 }) = &nv.value
403 {
404 let line = s.value();
405 let trimmed = line.strip_prefix(' ').unwrap_or(&line);
406 doc_lines.push(trimmed.to_string());
407 }
408 }
409 let doc = doc_lines.join("\n");
410
411 let display_name_lit = LitStr::new(&display_name, name_ident.span());
412 let doc_lit = LitStr::new(&doc, name_ident.span());
413 let link_name = format_ident!(
414 "__LATTICE_GROUP_DECL_{}",
415 uppercase(&name_ident.to_string())
416 );
417
418 Ok(quote! {
419 #[doc = #doc_lit]
420 pub struct #name_ident;
421
422 impl ::lattice_config::OptionGroup for #name_ident {
423 const NAME: &'static str = #display_name_lit;
424 const DOC: &'static str = #doc_lit;
425 }
426
427 const _: () = {
429 assert!(
430 !::lattice_config::ends_with_mode_suffix(
431 <#name_ident as ::lattice_config::OptionGroup>::NAME
432 ),
433 "OptionGroup name must not end in `-mode` \
434 (modes-vs-groups disambiguation; \
435 `mode-architecture.md` §6.7.1)"
436 );
437 };
438
439 const _: () = {
440 #[::lattice_config::linkme::distributed_slice(
441 ::lattice_config::GROUP_DECLS
442 )]
443 #[linkme(crate = ::lattice_config::linkme)]
444 static #link_name: &::lattice_config::OptionGroupMetadata =
445 &::lattice_config::OptionGroupMetadata::for_group::<#name_ident>();
446 };
447 })
448}
449
450#[proc_macro]
475pub fn overrides(input: TokenStream) -> TokenStream {
476 let parsed = parse_macro_input!(input as OverridesInput);
477 match expand_overrides(&parsed) {
478 Ok(ts) => ts.into(),
479 Err(e) => e.to_compile_error().into(),
480 }
481}
482
483struct OverridesInput {
484 entries: Vec<OverrideEntry>,
485}
486
487struct OverrideEntry {
488 priority: Option<Ident>,
490 decl: Path,
491 value: Expr,
492}
493
494impl Parse for OverridesInput {
495 fn parse(input: ParseStream) -> syn::Result<Self> {
496 let mut entries = Vec::new();
497 while !input.is_empty() {
498 let attrs = input.call(Attribute::parse_outer)?;
501 let priority = parse_priority(&attrs)?;
502 let decl: Path = input.parse()?;
503 let _: Token![=] = input.parse()?;
504 let value: Expr = input.parse()?;
505 if !input.is_empty() {
507 let _: Token![,] = input.parse()?;
508 }
509 entries.push(OverrideEntry {
510 priority,
511 decl,
512 value,
513 });
514 }
515 Ok(Self { entries })
516 }
517}
518
519fn parse_priority(attrs: &[Attribute]) -> syn::Result<Option<Ident>> {
520 for attr in attrs {
521 if attr.path().is_ident("priority") {
522 let ident: Ident = attr.parse_args()?;
523 if ident != "High" && ident != "Low" && ident != "Normal" {
525 return Err(syn::Error::new(
526 ident.span(),
527 "expected `High`, `Low`, or `Normal`",
528 ));
529 }
530 return Ok(Some(ident));
531 }
532 }
533 Ok(None)
534}
535
536fn expand_overrides(input: &OverridesInput) -> syn::Result<TokenStream2> {
537 let pushes: Vec<TokenStream2> = input
538 .entries
539 .iter()
540 .map(|entry| {
541 let decl = &entry.decl;
542 let value = &entry.value;
543 if let Some(priority) = &entry.priority {
544 quote! {
545 {
546 let __value: <#decl as ::lattice_config::OptionDecl>::Value = #value;
551 __set.push(
552 ::lattice_config::OptionOverride::with_priority(
553 ::std::any::TypeId::of::<#decl>(),
554 __value,
555 ::lattice_config::OverridePriority::#priority,
556 )
557 );
558 }
559 }
560 } else {
561 quote! {
562 {
563 let __value: <#decl as ::lattice_config::OptionDecl>::Value = #value;
564 __set.push(
565 ::lattice_config::OptionOverride::new(
566 ::std::any::TypeId::of::<#decl>(),
567 __value,
568 )
569 );
570 }
571 }
572 }
573 })
574 .collect();
575
576 Ok(quote! {{
577 let mut __set = ::lattice_config::OptionOverrideSet::new();
578 #(#pushes)*
579 __set
580 }})
581}
582
583fn camel_to_kebab(s: &str) -> String {
601 let mut out = String::with_capacity(s.len() + 4);
602 for (i, c) in s.chars().enumerate() {
603 if c.is_ascii_uppercase() {
604 if i != 0 && !out.ends_with('-') {
609 out.push('-');
610 }
611 out.push(c.to_ascii_lowercase());
612 } else if c == '_' {
613 if !out.ends_with('-') {
616 out.push('-');
617 }
618 } else {
619 out.push(c);
620 }
621 }
622 out
623}
624
625fn uppercase(s: &str) -> String {
626 s.to_uppercase()
627}
628
629#[cfg(test)]
630mod tests {
631 use super::camel_to_kebab;
632
633 #[test]
634 fn kebab_basic() {
635 assert_eq!(camel_to_kebab("Tabstop"), "tabstop");
636 assert_eq!(camel_to_kebab("RelativeNumber"), "relative-number");
637 assert_eq!(
638 camel_to_kebab("CompletionGhostText"),
639 "completion-ghost-text"
640 );
641 assert_eq!(camel_to_kebab("Number"), "number");
642 }
643
644 #[test]
645 fn kebab_with_underscore() {
646 assert_eq!(camel_to_kebab("Auto_Insert"), "auto-insert");
649 }
650
651 #[test]
652 fn kebab_acronyms() {
653 assert_eq!(camel_to_kebab("LSP"), "l-s-p");
658 assert_eq!(camel_to_kebab("UIRender"), "u-i-render");
659 }
660}