diff options
| -rw-r--r-- | mingling/Cargo.toml | 3 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 6 | ||||
| -rw-r--r-- | mingling_core/Cargo.toml | 3 | ||||
| -rw-r--r-- | mingling_core/src/asset.rs | 7 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp.rs | 20 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/flags.rs | 35 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/shell_ctx.rs | 180 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/suggest.rs | 159 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 2 | ||||
| -rw-r--r-- | mingling_macros/Cargo.toml | 2 | ||||
| -rw-r--r-- | mingling_macros/src/completion.rs | 175 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 16 | ||||
| -rw-r--r-- | mingling_macros/src/suggest.rs | 72 |
13 files changed, 678 insertions, 2 deletions
diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index 3d19be4..09b490b 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -17,13 +17,14 @@ mingling = { path = ".", features = ["full"] } [features] default = ["mingling_core/default"] -full = ["mingling_core/full", "general_renderer", "parser"] +full = ["mingling_core/full", "mingling_macros/full", "comp", "parser"] general_renderer = [ "mingling_core/general_renderer", "dep:serde", "mingling_macros/general_renderer", ] +comp = ["mingling_core/comp", "mingling_macros/comp"] parser = ["dep:size"] [dependencies] diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index eb1d970..140d563 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -67,6 +67,9 @@ pub mod parser; pub mod macros { /// Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; + /// Used to generate completion entry + #[cfg(feature = "comp")] + pub use mingling_macros::completion; /// Used to create a dispatcher that routes to a `Chain` pub use mingling_macros::dispatcher; /// Used to create a dispatcher that routes to a `Renderer` @@ -85,6 +88,9 @@ pub mod macros { pub use mingling_macros::r_println; /// Used to generate a struct implementing the `Renderer` trait via a method pub use mingling_macros::renderer; + #[cfg(feature = "comp")] + /// Used to generate suggestions + pub use mingling_macros::suggest; } /// derive macro Groupped diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml index e978401..ef0ea5d 100644 --- a/mingling_core/Cargo.toml +++ b/mingling_core/Cargo.toml @@ -8,7 +8,8 @@ repository = "https://github.com/catilgrass/mingling" [features] default = [] -full = ["general_renderer"] +full = ["comp", "general_renderer"] +comp = [] general_renderer = [ "dep:serde", "dep:ron", diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index 81aa3a6..1270a50 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -1,8 +1,15 @@ #[doc(hidden)] pub mod chain; + +#[cfg(feature = "comp")] +#[doc(hidden)] +pub mod comp; + #[doc(hidden)] pub mod dispatcher; + #[doc(hidden)] pub mod node; + #[doc(hidden)] pub mod renderer; diff --git a/mingling_core/src/asset/comp.rs b/mingling_core/src/asset/comp.rs new file mode 100644 index 0000000..4815b5a --- /dev/null +++ b/mingling_core/src/asset/comp.rs @@ -0,0 +1,20 @@ +mod flags; +mod shell_ctx; +mod suggest; + +#[doc(hidden)] +pub use flags::*; +#[doc(hidden)] +pub use shell_ctx::*; +#[doc(hidden)] +pub use suggest::*; + +/// Trait for implementing completion logic. +/// +/// This trait defines the interface for generating command-line completions. +/// Types implementing this trait can provide custom completion suggestions +/// based on the current shell context. +pub trait Completion { + type Previous; + fn comp(ctx: ShellContext) -> Suggest; +} diff --git a/mingling_core/src/asset/comp/flags.rs b/mingling_core/src/asset/comp/flags.rs new file mode 100644 index 0000000..b432b08 --- /dev/null +++ b/mingling_core/src/asset/comp/flags.rs @@ -0,0 +1,35 @@ +use just_fmt::snake_case; + +#[derive(Default, Debug, Clone)] +pub enum ShellFlag { + #[default] + Bash, + Zsh, + Fish, + Powershell, + Other(String), +} + +impl From<String> for ShellFlag { + fn from(s: String) -> Self { + match s.trim().to_lowercase().as_str() { + "zsh" => ShellFlag::Zsh, + "bash" => ShellFlag::Bash, + "fish" => ShellFlag::Fish, + "pwsl" | "ps1" | "powershell" => ShellFlag::Powershell, + other => ShellFlag::Other(snake_case!(other)), + } + } +} + +impl From<ShellFlag> for String { + fn from(flag: ShellFlag) -> Self { + match flag { + ShellFlag::Zsh => "zsh".to_string(), + ShellFlag::Bash => "bash".to_string(), + ShellFlag::Fish => "fish".to_string(), + ShellFlag::Powershell => "powershell".to_string(), + ShellFlag::Other(s) => s, + } + } +} diff --git a/mingling_core/src/asset/comp/shell_ctx.rs b/mingling_core/src/asset/comp/shell_ctx.rs new file mode 100644 index 0000000..081337f --- /dev/null +++ b/mingling_core/src/asset/comp/shell_ctx.rs @@ -0,0 +1,180 @@ +use crate::ShellFlag; + +/// Context passed from the shell to the completion system, +/// providing information about the current command line state +/// to guide how completions should be generated. +#[derive(Default, Debug)] +pub struct ShellContext { + /// The full command line (-f / --command-line) + pub command_line: String, + + /// Cursor position (-C / --cursor-position) + pub cursor_position: usize, + + /// Current word (-w / --current-word) + pub current_word: String, + + /// Previous word (-p / --previous-word) + pub previous_word: String, + + /// Command name (-c / --command-name) + pub command_name: String, + + /// Word index (-i / --word-index) + pub word_index: usize, + + /// All words (-a / --all-words) + pub all_words: Vec<String>, + + /// Flag to indicate completion context (-F / --shell-flag) + pub shell_flag: ShellFlag, +} + +impl TryFrom<Vec<String>> for ShellContext { + type Error = String; + + fn try_from(args: Vec<String>) -> Result<Self, Self::Error> { + use std::collections::HashMap; + + // Parse arguments into a map for easy lookup + let mut arg_map = HashMap::new(); + let mut i = 0; + while i < args.len() { + if args[i].starts_with('-') { + let key = args[i].clone(); + if i + 1 < args.len() && !args[i + 1].starts_with('-') { + arg_map.insert(key, args[i + 1].clone()); + i += 2; + } else { + arg_map.insert(key, String::new()); + i += 1; + } + } else { + i += 1; + } + } + + // Extract values with defaults + let command_line = arg_map.get("-f").cloned().unwrap_or_default(); + let cursor_position = arg_map + .get("-C") + .and_then(|s| s.parse().ok()) + .unwrap_or_default(); + let current_word = arg_map.get("-w").cloned().unwrap_or_default(); + let previous_word = arg_map.get("-p").cloned().unwrap_or_default(); + let command_name = arg_map.get("-c").cloned().unwrap_or_default(); + let word_index = arg_map + .get("-i") + .and_then(|s| s.parse().ok()) + .unwrap_or_default(); + let shell_flag = arg_map + .get("-F") + .cloned() + .map(ShellFlag::from) + .unwrap_or(ShellFlag::Other("unknown".to_string())); + + // Build all_words from command_line using basic whitespace splitting + // Note: External input replaces '-' with '^' in arguments, so we need to restore them + let all_words = command_line + .split_whitespace() + .map(|s| s.replace('^', "-")) + .collect(); + + // Also restore the original command_line with proper hyphens + let command_line = command_line.replace('^', "-"); + + Ok(ShellContext { + command_line, + cursor_position, + current_word, + previous_word, + command_name, + word_index, + all_words, + shell_flag, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_from_full_args() { + let args = vec![ + "-f".to_string(), + "git commit ^m 'test'".to_string(), + "-C".to_string(), + "12".to_string(), + "-w".to_string(), + "commit".to_string(), + "-p".to_string(), + "git".to_string(), + "-c".to_string(), + "git".to_string(), + "-i".to_string(), + "1".to_string(), + "-F".to_string(), + "bash".to_string(), + ]; + + let context = ShellContext::try_from(args).unwrap(); + assert_eq!(context.command_line, "git commit -m 'test'"); + assert_eq!(context.cursor_position, 12); + assert_eq!(context.current_word, "commit"); + assert_eq!(context.previous_word, "git"); + assert_eq!(context.command_name, "git"); + assert_eq!(context.word_index, 1); + assert_eq!(context.all_words, vec!["git", "commit", "-m", "'test'"]); + assert!(matches!(context.shell_flag, ShellFlag::Bash)); + } + + #[test] + fn test_try_from_partial_args() { + let args = vec![ + "-f".to_string(), + "ls ^la".to_string(), + "-C".to_string(), + "5".to_string(), + ]; + + let context = ShellContext::try_from(args).unwrap(); + assert_eq!(context.command_line, "ls -la"); + assert_eq!(context.cursor_position, 5); + assert_eq!(context.current_word, ""); + assert_eq!(context.previous_word, ""); + assert_eq!(context.command_name, ""); + assert_eq!(context.word_index, 0); + assert_eq!(context.all_words, vec!["ls", "-la"]); + assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == "unknown")); + } + + #[test] + fn test_try_from_empty_args() { + let args = vec![]; + let context = ShellContext::try_from(args).unwrap(); + assert_eq!(context.command_line, ""); + assert_eq!(context.cursor_position, 0); + assert_eq!(context.current_word, ""); + assert_eq!(context.previous_word, ""); + assert_eq!(context.command_name, ""); + assert_eq!(context.word_index, 0); + assert!(context.all_words.is_empty()); + assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == "unknown")); + } + + #[test] + fn test_try_from_flag_without_value() { + let args = vec!["-F".to_string()]; + let context = ShellContext::try_from(args).unwrap(); + assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == "")); + } + + #[test] + fn test_all_words_splitting() { + let args = vec!["-f".to_string(), " cmd arg1 arg2 ".to_string()]; + let context = ShellContext::try_from(args).unwrap(); + assert_eq!(context.all_words, vec!["cmd", "arg1", "arg2"]); + } +} diff --git a/mingling_core/src/asset/comp/suggest.rs b/mingling_core/src/asset/comp/suggest.rs new file mode 100644 index 0000000..4e7ce82 --- /dev/null +++ b/mingling_core/src/asset/comp/suggest.rs @@ -0,0 +1,159 @@ +use std::collections::BTreeSet; + +/// A completion suggestion that tells the shell how to perform completion. +/// This can be either a set of specific suggestion items or a request for file completion. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub enum Suggest { + /// A set of specific suggestion items for the shell to display. + Suggest(BTreeSet<SuggestItem>), + + /// A request for the shell to perform fileāpath completion. + #[default] + FileCompletion, +} + +impl Suggest { + /// Creates a new Suggest variant containing a BTreeSet of suggestions. + pub fn new() -> Self { + Self::Suggest(BTreeSet::new()) + } + + /// Creates a FileCompletion variant. + pub fn file_comp() -> Self { + Self::FileCompletion + } +} + +impl<T> From<T> for Suggest +where + T: IntoIterator, + T::Item: Into<String>, +{ + fn from(items: T) -> Self { + let suggests = items + .into_iter() + .map(|item| SuggestItem::new(item.into())) + .collect(); + Suggest::Suggest(suggests) + } +} + +impl std::ops::Deref for Suggest { + type Target = BTreeSet<SuggestItem>; + + fn deref(&self) -> &Self::Target { + match self { + Self::Suggest(suggests) => suggests, + Self::FileCompletion => panic!("Cannot deref FileCompletion variant"), + } + } +} + +impl std::ops::DerefMut for Suggest { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Suggest(suggests) => suggests, + Self::FileCompletion => panic!("Cannot deref_mut FileCompletion variant"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SuggestItem { + Simple(String), + WithDescription(String, String), +} + +impl Default for SuggestItem { + fn default() -> Self { + SuggestItem::Simple(String::new()) + } +} + +impl PartialOrd for SuggestItem { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for SuggestItem { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.suggest().cmp(&other.suggest()) + } +} + +impl SuggestItem { + /// Creates a new simple suggestion without description. + pub fn new(suggest: String) -> Self { + Self::Simple(suggest) + } + + /// Creates a new suggestion with a description. + pub fn new_with_desc(suggest: String, description: String) -> Self { + Self::WithDescription(suggest, description) + } + + /// Adds a description to this suggestion, replacing any existing description. + pub fn with_desc(self, description: String) -> Self { + match self { + Self::Simple(suggest) => Self::WithDescription(suggest, description), + Self::WithDescription(suggest, _) => Self::WithDescription(suggest, description), + } + } + + /// Returns the suggestion text. + pub fn suggest(&self) -> &String { + match self { + Self::Simple(suggest) => suggest, + Self::WithDescription(suggest, _) => suggest, + } + } + + /// Updates the suggestion text. + pub fn set_suggest(&mut self, new_suggest: String) { + match self { + Self::Simple(suggest) => *suggest = new_suggest, + Self::WithDescription(suggest, _) => *suggest = new_suggest, + } + } + + /// Returns the description if present. + pub fn description(&self) -> Option<&String> { + match self { + Self::Simple(_) => None, + Self::WithDescription(_, description) => Some(description), + } + } + + /// Sets or replaces the description. + pub fn set_description(&mut self, description: String) { + match self { + Self::Simple(suggest) => *self = Self::WithDescription(suggest.clone(), description), + Self::WithDescription(_, desc) => *desc = description, + } + } + + /// Removes and returns the description if present. + pub fn remove_desc(&mut self) -> Option<String> { + match self { + Self::Simple(_) => None, + Self::WithDescription(suggest, description) => { + let desc = std::mem::take(description); + *self = Self::Simple(std::mem::take(suggest)); + Some(desc) + } + } + } +} + +impl From<String> for SuggestItem { + fn from(suggest: String) -> Self { + Self::new(suggest) + } +} + +impl From<(String, String)> for SuggestItem { + fn from((suggest, description): (String, String)) -> Self { + Self::new_with_desc(suggest, description) + } +} diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 999c141..072f50e 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -21,6 +21,8 @@ pub use crate::any::group::*; pub use crate::any::*; pub use crate::asset::chain::*; +#[cfg(feature = "comp")] +pub use crate::asset::comp::*; pub use crate::asset::dispatcher::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml index 8c42299..d2c1a00 100644 --- a/mingling_macros/Cargo.toml +++ b/mingling_macros/Cargo.toml @@ -11,6 +11,8 @@ proc-macro = true [features] default = [] +full = ["comp", "general_renderer"] +comp = [] general_renderer = [] [dependencies] diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/completion.rs new file mode 100644 index 0000000..cf66f13 --- /dev/null +++ b/mingling_macros/src/completion.rs @@ -0,0 +1,175 @@ +//! Completion Attribute Macro Implementation +//! +//! This module provides the `#[completion]` attribute macro for automatically +//! generating structs that implement the `Completion` trait from functions. + +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ + FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input, +}; + +/// Extracts the previous type from function arguments +fn extract_previous_type(sig: &Signature) -> syn::Result<TypePath> { + // The function should have exactly one parameter: ShellContext + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Completion function must have exactly one parameter (ShellContext)", + )); + } + + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { ty, .. }) => { + match &**ty { + Type::Path(type_path) => { + // Check if it's ShellContext + let last_segment = type_path.path.segments.last().unwrap(); + if last_segment.ident != "ShellContext" { + return Err(syn::Error::new( + ty.span(), + "Parameter type must be ShellContext", + )); + } + Ok(type_path.clone()) + } + _ => Err(syn::Error::new( + ty.span(), + "Parameter type must be a type path", + )), + } + } + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Completion function cannot have self parameter", + )), + } +} + +/// Extracts the return type from the function signature +fn extract_return_type(sig: &Signature) -> syn::Result<TypePath> { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Path(type_path) => { + // Check if it's Suggest + let last_segment = type_path.path.segments.last().unwrap(); + if last_segment.ident != "Suggest" { + return Err(syn::Error::new(ty.span(), "Return type must be Suggest")); + } + Ok(type_path.clone()) + } + _ => Err(syn::Error::new( + ty.span(), + "Return type must be a type path", + )), + }, + ReturnType::Default => Err(syn::Error::new( + sig.span(), + "Completion function must have a return type", + )), + } +} + +/// Extracts the parameter name from function arguments +fn extract_param_name(sig: &Signature) -> syn::Result<Pat> { + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Completion function must have exactly one parameter", + )); + } + + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { pat, .. }) => Ok((**pat).clone()), + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Completion function cannot have self parameter", + )), + } +} + +#[cfg(feature = "comp")] +pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the attribute arguments (e.g., HelloEntry from #[completion(HelloEntry)]) + let previous_type_ident = if attr.is_empty() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "completion attribute requires a previous type argument, e.g. #[completion(HelloEntry)]", + ) + .to_compile_error() + .into(); + } else { + parse_macro_input!(attr as Ident) + }; + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Completion function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the parameter name + let param_name = match extract_param_name(&input_fn.sig) { + Ok(name) => name, + Err(e) => return e.to_compile_error().into(), + }; + + // Extract and validate the parameter type (must be ShellContext) + if let Err(e) = extract_previous_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Extract and validate the return type (must be Suggest) + if let Err(e) = extract_return_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Get the function body + let fn_body = &input_fn.block; + + // Get function attributes (excluding the completion attribute) + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("completion")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string()); + let struct_name = Ident::new(&pascal_case_name, fn_name.span()); + + // Generate the struct and implementation + let expanded = quote! { + #(#fn_attrs)* + #vis struct #struct_name; + + impl ::mingling::Completion for #struct_name { + type Previous = #previous_type_ident; + + fn comp(#param_name: ::mingling::ShellContext) -> ::mingling::Suggest { + // This is just to prevent warnings about imported ShellContext and Suggest + let _ = ShellContext::default(); + let _ = Suggest::file_comp(); + #fn_body + } + } + + // Keep the original function for internal use + #(#fn_attrs)* + #vis fn #fn_name(#param_name: ::mingling::ShellContext) -> ::mingling::Suggest { + #fn_body + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 4341669..ff43482 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -9,6 +9,8 @@ use quote::quote; use syn::parse_macro_input; mod chain; +#[cfg(feature = "comp")] +mod completion; mod dispatcher_chain; mod groupped; mod node; @@ -16,6 +18,8 @@ mod pack; mod program_setup; mod render; mod renderer; +#[cfg(feature = "comp")] +mod suggest; use once_cell::sync::Lazy; use std::sync::Mutex; @@ -70,6 +74,12 @@ pub fn renderer(_attr: TokenStream, item: TokenStream) -> TokenStream { renderer::renderer_attr(item) } +#[cfg(feature = "comp")] +#[proc_macro_attribute] +pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream { + completion::completion_attr(attr, item) +} + #[proc_macro_attribute] pub fn program_setup(attr: TokenStream, item: TokenStream) -> TokenStream { program_setup::setup_attr(attr, item) @@ -243,3 +253,9 @@ pub fn __register_renderer(input: TokenStream) -> TokenStream { TokenStream::new() } + +#[cfg(feature = "comp")] +#[proc_macro] +pub fn suggest(input: TokenStream) -> TokenStream { + suggest::suggest(input) +} diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/suggest.rs new file mode 100644 index 0000000..886eee0 --- /dev/null +++ b/mingling_macros/src/suggest.rs @@ -0,0 +1,72 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Expr, LitStr, Token, parse_macro_input}; + +struct SuggestInput { + items: Punctuated<SuggestItem, Token![,]>, +} + +enum SuggestItem { + WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" + Simple(LitStr), // "-I" +} + +impl Parse for SuggestInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let items = Punctuated::parse_terminated(input)?; + Ok(SuggestInput { items }) + } +} + +impl Parse for SuggestItem { + fn parse(input: ParseStream) -> syn::Result<Self> { + let key: LitStr = input.parse()?; + + if input.peek(Token![:]) { + let _colon: Token![:] = input.parse()?; + let value: Expr = input.parse()?; + Ok(SuggestItem::WithDesc(Box::new((key, value)))) + } else { + Ok(SuggestItem::Simple(key)) + } + } +} + +#[cfg(feature = "comp")] +pub fn suggest(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as SuggestInput); + + let mut items = Vec::new(); + + for item in input.items { + match item { + SuggestItem::WithDesc(boxed) => { + let (key, value) = *boxed; + items.push(quote! { + ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string()) + }); + } + SuggestItem::Simple(key) => { + items.push(quote! { + ::mingling::SuggestItem::new(#key.to_string()) + }); + } + } + } + + let expanded = if items.is_empty() { + quote! { + ::mingling::Suggest::default() + } + } else { + quote! {{ + let mut suggest = ::mingling::Suggest::default(); + #(suggest.insert(#items);)* + suggest + }} + }; + + expanded.into() +} |
