aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mingling/Cargo.toml3
-rw-r--r--mingling/src/lib.rs6
-rw-r--r--mingling_core/Cargo.toml3
-rw-r--r--mingling_core/src/asset.rs7
-rw-r--r--mingling_core/src/asset/comp.rs20
-rw-r--r--mingling_core/src/asset/comp/flags.rs35
-rw-r--r--mingling_core/src/asset/comp/shell_ctx.rs180
-rw-r--r--mingling_core/src/asset/comp/suggest.rs159
-rw-r--r--mingling_core/src/lib.rs2
-rw-r--r--mingling_macros/Cargo.toml2
-rw-r--r--mingling_macros/src/completion.rs175
-rw-r--r--mingling_macros/src/lib.rs16
-rw-r--r--mingling_macros/src/suggest.rs72
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()
+}