diff options
| -rw-r--r-- | mingling_core/src/asset/comp.rs | 32 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/flags.rs | 1 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/shell_ctx.rs | 1 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/suggest.rs | 2 | ||||
| -rw-r--r-- | mingling_core/src/program.rs | 8 | ||||
| -rw-r--r-- | mingling_macros/src/completion.rs | 132 | ||||
| -rw-r--r-- | mingling_macros/src/dispatcher_chain.rs | 38 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 67 |
8 files changed, 175 insertions, 106 deletions
diff --git a/mingling_core/src/asset/comp.rs b/mingling_core/src/asset/comp.rs index 4815b5a..eeef0c0 100644 --- a/mingling_core/src/asset/comp.rs +++ b/mingling_core/src/asset/comp.rs @@ -2,6 +2,8 @@ mod flags; mod shell_ctx; mod suggest; +use std::fmt::Display; + #[doc(hidden)] pub use flags::*; #[doc(hidden)] @@ -9,6 +11,8 @@ pub use shell_ctx::*; #[doc(hidden)] pub use suggest::*; +use crate::{ProgramCollect, this}; + /// Trait for implementing completion logic. /// /// This trait defines the interface for generating command-line completions. @@ -16,5 +20,31 @@ pub use suggest::*; /// based on the current shell context. pub trait Completion { type Previous; - fn comp(ctx: ShellContext) -> Suggest; + fn comp(ctx: &ShellContext) -> Suggest; +} + +/// Trait for extracting user input arguments for completion. +/// +/// When the `feat comp` feature is enabled, the `dispatcher!` macro will +/// automatically implement this trait for `Entry` types to extract the +/// arguments from user input for completion suggestions. +pub trait CompletionEntry { + fn get_input(self) -> Vec<String>; +} + +pub struct CompletionHelper; +impl CompletionHelper { + pub fn exec_completion<P>(ctx: &ShellContext) -> Suggest + where + P: ProgramCollect + Display + 'static, + { + let program = this::<P>(); + Suggest::FileCompletion + } + + pub fn render_suggest<P>(ctx: ShellContext, suggest: Suggest) + where + P: ProgramCollect + Display + 'static, + { + } } diff --git a/mingling_core/src/asset/comp/flags.rs b/mingling_core/src/asset/comp/flags.rs index b432b08..0762d0d 100644 --- a/mingling_core/src/asset/comp/flags.rs +++ b/mingling_core/src/asset/comp/flags.rs @@ -1,6 +1,7 @@ use just_fmt::snake_case; #[derive(Default, Debug, Clone)] +#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))] pub enum ShellFlag { #[default] Bash, diff --git a/mingling_core/src/asset/comp/shell_ctx.rs b/mingling_core/src/asset/comp/shell_ctx.rs index 081337f..4771e63 100644 --- a/mingling_core/src/asset/comp/shell_ctx.rs +++ b/mingling_core/src/asset/comp/shell_ctx.rs @@ -4,6 +4,7 @@ use crate::ShellFlag; /// providing information about the current command line state /// to guide how completions should be generated. #[derive(Default, Debug)] +#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))] pub struct ShellContext { /// The full command line (-f / --command-line) pub command_line: String, diff --git a/mingling_core/src/asset/comp/suggest.rs b/mingling_core/src/asset/comp/suggest.rs index 4e7ce82..55a874f 100644 --- a/mingling_core/src/asset/comp/suggest.rs +++ b/mingling_core/src/asset/comp/suggest.rs @@ -3,6 +3,7 @@ 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)] +#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))] pub enum Suggest { /// A set of specific suggestion items for the shell to display. Suggest(BTreeSet<SuggestItem>), @@ -59,6 +60,7 @@ impl std::ops::DerefMut for Suggest { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))] pub enum SuggestItem { Simple(String), WithDescription(String, String), diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs index 8c8b4cb..7b9f8d4 100644 --- a/mingling_core/src/program.rs +++ b/mingling_core/src/program.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "comp")] +use crate::{ShellContext, Suggest}; + #[cfg(feature = "general_renderer")] use crate::error::GeneralRendererSerializeError; + use crate::{ AnyOutput, ChainProcess, RenderResult, asset::dispatcher::Dispatcher, error::ProgramExecuteError, @@ -183,6 +187,10 @@ pub trait ProgramCollect { any: AnyOutput<Self::Enum>, ) -> Pin<Box<dyn Future<Output = ChainProcess<Self::Enum>> + Send>>; + /// Match and execute specific completion logic based on any Entry + #[cfg(feature = "comp")] + fn do_comp(any: &AnyOutput<Self::Enum>, ctx: &ShellContext) -> Suggest; + /// Whether the program has a renderer that can handle the current [AnyOutput](./struct.AnyOutput.html) fn has_renderer(any: &AnyOutput<Self::Enum>) -> bool; diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/completion.rs index cf66f13..23b509f 100644 --- a/mingling_macros/src/completion.rs +++ b/mingling_macros/src/completion.rs @@ -5,91 +5,7 @@ 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", - )), - } -} +use syn::{Ident, ItemFn, parse_macro_input}; #[cfg(feature = "comp")] pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -110,25 +26,28 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Validate the function is not async if input_fn.sig.asyncness.is_some() { + use syn::spanned::Spanned; + 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(), - }; + // Get the function signature parts + let sig = &input_fn.sig; + let inputs = &sig.inputs; + let output = &sig.output; - // 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(); - } + // Check that the function has exactly one parameter + if inputs.len() != 1 { + use syn::spanned::Spanned; - // 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(); + return syn::Error::new( + inputs.span(), + "Completion function must have exactly one parameter", + ) + .to_compile_error() + .into(); } // Get the function body @@ -142,7 +61,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let vis = &input_fn.vis; // Get function name - let fn_name = &input_fn.sig.ident; + let fn_name = &sig.ident; // Generate struct name from function name using pascal_case let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string()); @@ -156,20 +75,27 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { 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 comp(#inputs) #output { #fn_body } } // Keep the original function for internal use #(#fn_attrs)* - #vis fn #fn_name(#param_name: ::mingling::ShellContext) -> ::mingling::Suggest { + #vis fn #fn_name(#inputs) #output { #fn_body } }; + let completion_entry = quote! { + Self::#previous_type_ident => <#struct_name as ::mingling::Completion>::comp(ctx), + }; + + let mut completions = crate::COMPLETIONS.lock().unwrap(); + let completion_str = completion_entry.to_string(); + if !completions.contains(&completion_str) { + completions.push(completion_str); + } + expanded.into() } diff --git a/mingling_macros/src/dispatcher_chain.rs b/mingling_macros/src/dispatcher_chain.rs index f531424..4038600 100644 --- a/mingling_macros/src/dispatcher_chain.rs +++ b/mingling_macros/src/dispatcher_chain.rs @@ -1,7 +1,7 @@ //! Dispatcher Chain and Dispatcher Render Macros //! //! This module provides macros for creating dispatcher chain and dispatcher render structs -//! with automatic implementations of the `DispatcherChain` trait. +//! with automatic implementations of the `Dispatcher` trait. use proc_macro::TokenStream; use quote::quote; @@ -60,6 +60,13 @@ impl Parse for DispatcherChainInput { } } +// NOTICE: This implementation contains significant code duplication between the explicit +// and default cases in both `dispatcher_chain` and `dispatcher_render` functions. +// The logic for handling default vs explicit group names and generating the appropriate +// code should be extracted into common helper functions to reduce redundancy. +// Additionally, the token stream generation patterns are nearly identical between +// the two main functions and could benefit from refactoring. + pub fn dispatcher_chain(input: TokenStream) -> TokenStream { // Parse the input let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); @@ -87,6 +94,8 @@ pub fn dispatcher_chain(input: TokenStream) -> TokenStream { let command_name_str = command_name.value(); + let comp_entry = get_comp_entry(&pack); + let expanded = if use_default { // For default case, use ThisProgram quote! { @@ -95,6 +104,8 @@ pub fn dispatcher_chain(input: TokenStream) -> TokenStream { ::mingling::macros::pack!(ThisProgram, #pack = Vec<String>); + #comp_entry + impl ::mingling::Dispatcher<ThisProgram> for #command_struct { fn node(&self) -> ::mingling::Node { ::mingling::macros::node!(#command_name_str) @@ -115,6 +126,8 @@ pub fn dispatcher_chain(input: TokenStream) -> TokenStream { ::mingling::macros::pack!(#group_name, #pack = Vec<String>); + #comp_entry + impl ::mingling::Dispatcher<#group_name> for #command_struct { fn node(&self) -> ::mingling::Node { ::mingling::macros::node!(#command_name_str) @@ -159,6 +172,8 @@ pub fn dispatcher_render(input: TokenStream) -> TokenStream { let command_name_str = command_name.value(); + let comp_entry = get_comp_entry(&pack); + let expanded = if use_default { // For default case, use ThisProgram quote! { @@ -167,6 +182,8 @@ pub fn dispatcher_render(input: TokenStream) -> TokenStream { ::mingling::macros::pack!(ThisProgram, #pack = Vec<String>); + #comp_entry + impl ::mingling::Dispatcher for #command_struct { fn node(&self) -> ::mingling::Node { ::mingling::macros::node!(#command_name_str) @@ -187,6 +204,8 @@ pub fn dispatcher_render(input: TokenStream) -> TokenStream { ::mingling::macros::pack!(#group_name, #pack = Vec<String>); + #comp_entry + impl ::mingling::Dispatcher for #command_struct { fn node(&self) -> ::mingling::Node { ::mingling::macros::node!(#command_name_str) @@ -203,3 +222,20 @@ pub fn dispatcher_render(input: TokenStream) -> TokenStream { expanded.into() } + +#[cfg(feature = "comp")] +fn get_comp_entry(entry_name: &Ident) -> proc_macro2::TokenStream { + let comp_entry = quote! { + impl ::mingling::CompletionEntry for #entry_name { + fn get_input(self) -> Vec<String> { + self.inner.clone() + } + } + }; + comp_entry +} + +#[cfg(not(feature = "comp"))] +fn get_comp_entry(_entry_name: &Ident) -> proc_macro2::TokenStream { + quote! {} +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index d324ddc..7291f0e 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -24,10 +24,13 @@ mod suggest; use once_cell::sync::Lazy; use std::sync::Mutex; -// Global variable declarations for storing chain and renderer mappings +// Global variables #[cfg(feature = "general_renderer")] pub(crate) static GENERAL_RENDERERS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); +#[cfg(feature = "comp")] +pub(crate) static COMPLETIONS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); + pub(crate) static PACKED_TYPES: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); pub(crate) static CHAINS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); pub(crate) static RENDERERS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); @@ -107,6 +110,13 @@ pub fn gen_program(input: TokenStream) -> TokenStream { let mut packed_types = PACKED_TYPES.lock().unwrap().clone(); packed_types.push("DispatcherNotFound".to_string()); packed_types.push("RendererNotFound".to_string()); + + #[cfg(feature = "comp")] + { + packed_types.push("CompletionContext".to_string()); + packed_types.push("CompletionSuggest".to_string()); + } + packed_types.sort(); packed_types.dedup(); let renderers = RENDERERS.lock().unwrap().clone(); @@ -117,6 +127,9 @@ pub fn gen_program(input: TokenStream) -> TokenStream { #[cfg(feature = "general_renderer")] let general_renderers = GENERAL_RENDERERS.lock().unwrap().clone(); + #[cfg(feature = "comp")] + let completions = COMPLETIONS.lock().unwrap().clone(); + let packed_types: Vec<proc_macro2::TokenStream> = packed_types .iter() .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) @@ -164,6 +177,55 @@ pub fn gen_program(input: TokenStream) -> TokenStream { #[cfg(not(feature = "general_renderer"))] let general_render = quote! {}; + #[cfg(feature = "comp")] + let comp_dispatcher = quote! { + ::mingling::macros::dispatcher!(#name, "__comp", CompletionDispatcher => CompletionContext); + ::mingling::macros::pack!( + #name, + CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) + ); + + #[::mingling::macros::chain] + async fn __completion(prev: CompletionContext) -> NextProcess { + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<#name>(&ctx); + CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + + #[::mingling::macros::renderer] + fn __render_completion(prev: CompletionSuggest) { + let (ctx, suggest) = prev.inner; + ::mingling::CompletionHelper::render_suggest::<#name>(ctx, suggest); + } + }; + + #[cfg(not(feature = "comp"))] + let comp_dispatcher = quote! {}; + + #[cfg(feature = "comp")] + let completion_tokens: Vec<proc_macro2::TokenStream> = completions + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { + match any.member_id { + #(#completion_tokens)* + _ => ::mingling::Suggest::FileCompletion, + } + } + }; + + #[cfg(not(feature = "comp"))] + let comp = quote! {}; + let expanded = quote! { ::mingling::macros::pack!(#name, RendererNotFound = String); ::mingling::macros::pack!(#name, DispatcherNotFound = Vec<String>); @@ -176,6 +238,8 @@ pub fn gen_program(input: TokenStream) -> TokenStream { #(#packed_types),* } + #comp_dispatcher + impl ::std::fmt::Display for #name { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { match self { @@ -212,6 +276,7 @@ pub fn gen_program(input: TokenStream) -> TokenStream { } } #general_render + #comp } impl #name { |
