diff options
| -rw-r--r-- | examples/example-basic/Cargo.lock | 4 | ||||
| -rw-r--r-- | examples/example-picker/Cargo.lock | 4 | ||||
| -rw-r--r-- | mingling/src/comp.rs | 157 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 13 | ||||
| -rw-r--r-- | mingling_cli/Cargo.lock | 4 | ||||
| -rw-r--r-- | mingling_core/src/asset.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/asset/enum_tag.rs | 11 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 1 | ||||
| -rw-r--r-- | mingling_macros/src/enum_tag.rs | 164 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 12 | ||||
| -rw-r--r-- | mingling_macros/src/suggest.rs | 21 |
11 files changed, 218 insertions, 176 deletions
diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock index 02c99b3..114e049 100644 --- a/examples/example-basic/Cargo.lock +++ b/examples/example-basic/Cargo.lock @@ -33,8 +33,6 @@ dependencies = [ [[package]] name = "mingling_core" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2b64cf9820a2ffd0ba1b1a6fdee99f1cce8e102e19d088cda796158b97ea45" dependencies = [ "just_fmt", "once_cell", @@ -45,8 +43,6 @@ dependencies = [ [[package]] name = "mingling_macros" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a053a1e0644b9dd1abd4117d080a12386d1fb7ade3103bbe0bca693fc659716" dependencies = [ "just_fmt", "once_cell", diff --git a/examples/example-picker/Cargo.lock b/examples/example-picker/Cargo.lock index 3147d64..7f22d09 100644 --- a/examples/example-picker/Cargo.lock +++ b/examples/example-picker/Cargo.lock @@ -34,8 +34,6 @@ dependencies = [ [[package]] name = "mingling_core" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2b64cf9820a2ffd0ba1b1a6fdee99f1cce8e102e19d088cda796158b97ea45" dependencies = [ "just_fmt", "once_cell", @@ -46,8 +44,6 @@ dependencies = [ [[package]] name = "mingling_macros" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a053a1e0644b9dd1abd4117d080a12386d1fb7ade3103bbe0bca693fc659716" dependencies = [ "just_fmt", "once_cell", diff --git a/mingling/src/comp.rs b/mingling/src/comp.rs deleted file mode 100644 index 7136ccc..0000000 --- a/mingling/src/comp.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::collections::HashSet; - -use mingling_core::{Flag, ShellContext, Suggest}; - -pub struct ShellContextHelper { - ctx: ShellContext, -} - -impl ShellContextHelper { - /// Checks if a flag appears exactly once in the command line arguments. - /// - /// This method is useful for determining whether a flag should be processed - /// when it should only be applied once, even if it appears multiple times - /// in the command line. It returns `true` if the flag is present and - /// appears exactly once among all words in the shell context. - /// - /// # Example - /// - /// ``` - /// # use mingling_core::ShellContext; - /// # use mingling_macros::suggest; - /// # use mingling::comp_tools::ShellContextHelper; - /// - /// let ctx = ShellContext::default(); - /// let helper = ShellContextHelper::from(ctx); - /// - /// // Check if either "--insert" or "-i" appears exactly once - /// if helper.filling_argument_first(["--insert", "-i"]) { - /// // Perform action that should only happen once, example: - /// // return suggest! { - /// // "A", "B", "C" - /// // } - /// } - /// ``` - pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool { - let flag = flag.into(); - if self.filling_argument(&flag) { - let mut flag_appears = 0; - for w in self.ctx.all_words.iter() { - for f in flag.iter() { - if *f == w { - flag_appears += 1; - } - } - } - if flag_appears < 2 { - return true; - } - } - return false; - } - - /// Checks if the previous word in the command line arguments matches any of the given flags. - /// - /// This method determines whether a flag is currently being processed - /// by checking the word immediately before the cursor position. It returns - /// `true` if the previous word matches any of the provided flag strings. - /// - /// # Example - /// - /// ``` - /// # use mingling_core::ShellContext; - /// # use mingling_macros::suggest; - /// # use mingling::comp_tools::ShellContextHelper; - /// - /// let ctx = ShellContext::default(); - /// let helper = ShellContextHelper::from(ctx); - /// - /// // Check if the previous word is either "--file" or "-f" - /// if helper.filling_argument(["--file", "-f"]) { - /// // The user is likely expecting a file argument next, example: - /// // return suggest! { - /// // "src/main.rs", "Cargo.toml", "README.md" - /// // } - /// } - /// ``` - pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool { - for f in flag.into().iter() { - if self.ctx.previous_word == **f { - return true; - } - } - return false; - } - - /// Checks if the user is currently typing a flag argument. - /// - /// This method determines whether the current word being typed starts with - /// a dash (`-`), indicating that the user is likely in the process of - /// entering a command-line flag. It returns `true` if the current word - /// begins with a dash character. - /// - /// # Example - /// - /// ``` - /// # use mingling_core::ShellContext; - /// # use mingling_macros::suggest; - /// # use mingling::comp_tools::ShellContextHelper; - /// - /// let ctx = ShellContext::default(); - /// let helper = ShellContextHelper::from(ctx); - /// - /// // Check if the user is typing a flag - /// if helper.typing_argument() { - /// // The user is likely entering a flag, example: - /// // return suggest! { - /// // "--help", "--version", "--verbose" - /// // } - /// } - /// ``` - pub fn typing_argument(&self) -> bool { - self.ctx.current_word.starts_with("-") - } - - /// Filters out already typed flag arguments from suggestion results. - /// - /// This method removes any suggestions that match flag arguments already present - /// in the command line. It is useful for preventing duplicate flag suggestions - /// when the user has already typed certain flags. The method processes both - /// regular suggestion sets and file completion suggestions differently. - pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest { - let typed = Self::get_typed_arguments(&self); - match suggest { - Suggest::Suggest(mut set) => { - set.retain(|item| !typed.contains(item.suggest())); - Suggest::Suggest(set) - } - Suggest::FileCompletion => Suggest::FileCompletion, - } - } - - /// Retrieves all flag arguments from the command line. - /// - /// This method collects all words in the shell context that start with a dash (`-`), - /// which typically represent command-line flags or options. It returns a vector - /// containing these flag strings, converted to owned `String` values. - pub fn get_typed_arguments(&self) -> HashSet<String> { - self.ctx - .all_words - .iter() - .filter(|word| word.starts_with("-")) - .map(|word| word.to_string()) - .collect() - } -} - -impl From<ShellContext> for ShellContextHelper { - fn from(ctx: ShellContext) -> Self { - Self { ctx } - } -} - -impl From<ShellContextHelper> for ShellContext { - fn from(helper: ShellContextHelper) -> Self { - helper.ctx - } -} diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index e2aa54a..5f45228 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -59,8 +59,6 @@ //! `Mingling` provides detailed usage examples for your reference. //! See [Examples](_mingling_examples/index.html) -#[cfg(feature = "comp")] -mod comp; mod example_docs; // Re-export Core lib @@ -105,16 +103,17 @@ pub mod macros { #[cfg(feature = "comp")] /// Used to generate suggestions pub use mingling_macros::suggest; + #[cfg(feature = "comp")] + /// Used to generate enum suggestions + pub use mingling_macros::suggest_enum; } +/// derive macro EnumTag +pub use mingling_macros::EnumTag; + /// derive macro Groupped pub use mingling_macros::Groupped; -#[cfg(feature = "comp")] -pub mod comp_tools { - pub use crate::comp::*; -} - /// Example projects for `Mingling`, for learning how to use `Mingling` pub mod _mingling_examples { pub use crate::example_docs::*; diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock index 8a0f2a2..737f437 100644 --- a/mingling_cli/Cargo.lock +++ b/mingling_cli/Cargo.lock @@ -72,8 +72,6 @@ dependencies = [ [[package]] name = "mingling_core" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2b64cf9820a2ffd0ba1b1a6fdee99f1cce8e102e19d088cda796158b97ea45" dependencies = [ "just_fmt", "once_cell", @@ -84,8 +82,6 @@ dependencies = [ [[package]] name = "mingling_macros" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a053a1e0644b9dd1abd4117d080a12386d1fb7ade3103bbe0bca693fc659716" dependencies = [ "just_fmt", "once_cell", diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index 1270a50..a4254ef 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -9,6 +9,9 @@ pub mod comp; pub mod dispatcher; #[doc(hidden)] +pub mod enum_tag; + +#[doc(hidden)] pub mod node; #[doc(hidden)] diff --git a/mingling_core/src/asset/enum_tag.rs b/mingling_core/src/asset/enum_tag.rs new file mode 100644 index 0000000..563d826 --- /dev/null +++ b/mingling_core/src/asset/enum_tag.rs @@ -0,0 +1,11 @@ +/// Marker trait for EnumTag +pub trait EnumTag { + /// Get the name and description of this enum + fn enum_info(&self) -> (&'static str, &'static str); + + /// Get all possible enum variant names and descriptions + fn enums() -> &'static [(&'static str, &'static str)]; + + /// Build the enum from a name + fn build_enum(name: String) -> Self; +} diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 2c5cf55..87be806 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -24,6 +24,7 @@ pub use crate::asset::chain::*; #[cfg(feature = "comp")] pub use crate::asset::comp::*; pub use crate::asset::dispatcher::*; +pub use crate::asset::enum_tag::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/enum_tag.rs new file mode 100644 index 0000000..a30a0d5 --- /dev/null +++ b/mingling_macros/src/enum_tag.rs @@ -0,0 +1,164 @@ +//! EnumTag derive macro implementation +//! +//! This module provides the `#[derive(EnumTag)]` procedural macro for enums. +//! The macro generates implementations of the `EnumTag` trait for enums with +//! unit variants only (no fields). Variants can have an optional `#[enum_desc]` +//! attribute to provide descriptions. + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{ + Attribute, Data, DeriveInput, Error, Expr, Fields, Ident, Lit, LitStr, Meta, MetaNameValue, + Result, Variant, parse_macro_input, +}; + +pub fn derive_enum_tag(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match derive_enum_tag_impl(input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +/// Implementation of the EnumTag derive macro +fn derive_enum_tag_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream> { + let enum_name = &input.ident; + let generics = &input.generics; + + // Extract enum data + let data = match input.data { + Data::Enum(data_enum) => data_enum, + Data::Struct(_) => { + return Err(Error::new_spanned( + enum_name, + "EnumTag can only be derived for enums, not structs", + )); + } + Data::Union(_) => { + return Err(Error::new_spanned( + enum_name, + "EnumTag can only be derived for enums, not unions", + )); + } + }; + + // Process each variant + let mut variant_info = Vec::new(); + let mut match_arms = Vec::new(); + let mut build_match_arms = Vec::new(); + + for variant in data.variants { + process_variant( + variant, + enum_name, + &mut variant_info, + &mut match_arms, + &mut build_match_arms, + )?; + } + + // Generate the implementation + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics ::mingling::EnumTag for #enum_name #ty_generics #where_clause { + fn enum_info(&self) -> (&'static str, &'static str) { + match self { + #(#match_arms)* + } + } + + fn build_enum(name: String) -> Self { + match name.as_str() { + #(#build_match_arms)* + _ => panic!("Invalid enum variant name: {}", name), + } + } + + fn enums() -> &'static [(&'static str, &'static str)] { + &[#(#variant_info),*] + } + } + }; + + Ok(expanded) +} + +/// Process a single enum variant +fn process_variant( + variant: Variant, + enum_name: &Ident, + variant_info: &mut Vec<proc_macro2::TokenStream>, + match_arms: &mut Vec<proc_macro2::TokenStream>, + build_match_arms: &mut Vec<proc_macro2::TokenStream>, +) -> Result<()> { + let variant_name = variant.ident.clone(); + + // Check if variant has fields + match &variant.fields { + Fields::Unit => { + // Good, unit variant + } + Fields::Named(_) | Fields::Unnamed(_) => { + return Err(Error::new_spanned( + &variant, + format!( + "EnumTag cannot be derived for enum variant `{}` with fields. Only unit variants are supported.", + variant_name + ), + )); + } + } + + // Extract description from #[enum_desc] attribute + let description = extract_description(&variant.attrs, &variant_name)?; + + // Generate tokens for this variant + let variant_name_str = variant_name.to_string(); + let description_str = description.value(); + + variant_info.push(quote! { + (#variant_name_str, #description_str) + }); + + match_arms.push(quote! { + #enum_name::#variant_name => (#variant_name_str, #description_str), + }); + + build_match_arms.push(quote! { + #variant_name_str => #enum_name::#variant_name, + }); + + Ok(()) +} + +/// Extract description from #[enum_desc] attribute +fn extract_description(attrs: &[Attribute], variant_name: &Ident) -> Result<LitStr> { + for attr in attrs { + if attr.path().is_ident("enum_desc") { + return match attr.parse_args::<Meta>() { + Ok(Meta::NameValue(MetaNameValue { + value: + Expr::Lit(syn::PatLit { + lit: Lit::Str(lit_str), + .. + }), + .. + })) => Ok(lit_str), + Ok(_) => Err(Error::new_spanned( + attr, + "#[enum_desc] attribute must be in the form `#[enum_desc(\"description\")]`", + )), + Err(_) => Err(Error::new_spanned( + attr, + "Failed to parse #[enum_desc] attribute", + )), + }; + } + } + + // If no #[enum_desc] attribute, use variant name as description + Ok(LitStr::new(&variant_name.to_string(), Span::call_site())) +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index f97c458..a989ce3 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -15,6 +15,7 @@ mod chain; #[cfg(feature = "comp")] mod completion; mod dispatcher_chain; +mod enum_tag; mod groupped; mod node; mod pack; @@ -98,6 +99,11 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream { groupped::derive_groupped(input) } +#[proc_macro_derive(EnumTag, attributes(enum_desc))] +pub fn derive_enum_tag(input: TokenStream) -> TokenStream { + enum_tag::derive_enum_tag(input) +} + #[cfg(feature = "general_renderer")] #[proc_macro_derive(GrouppedSerialize, attributes(group))] pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { @@ -343,6 +349,12 @@ pub fn suggest(input: TokenStream) -> TokenStream { suggest::suggest(input) } +#[cfg(feature = "comp")] +#[proc_macro] +pub fn suggest_enum(input: TokenStream) -> TokenStream { + suggest::suggest_enum(input) +} + fn read_name(input: &TokenStream) -> Ident { if input.is_empty() { Ident::new("ThisProgram", proc_macro2::Span::call_site()) diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/suggest.rs index 7354ff0..d3ab446 100644 --- a/mingling_macros/src/suggest.rs +++ b/mingling_macros/src/suggest.rs @@ -70,3 +70,24 @@ pub fn suggest(input: TokenStream) -> TokenStream { expanded.into() } + +pub fn suggest_enum(input: TokenStream) -> TokenStream { + let enum_type = parse_macro_input!(input as syn::Type); + + let expanded = quote! {{ + let mut enum_suggest = ::mingling::Suggest::new(); + for (name, desc) in <#enum_type>::enums() { + if desc.is_empty() { + enum_suggest.insert(::mingling::SuggestItem::new(name.to_string())); + } else { + enum_suggest.insert(::mingling::SuggestItem::new_with_desc( + name.to_string(), + desc.to_string(), + )); + } + } + enum_suggest + }}; + + expanded.into() +} |
