aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-04-10 16:47:40 +0800
committer魏曹先生 <1992414357@qq.com>2026-04-10 16:47:40 +0800
commitb18749170b6006e53976dbb6df9f59a3b9c34127 (patch)
treea0f9288fdc9082e26daab218167da1f54521d32b
parent3bb5afcbe01ad16293a66084dc1ad35f3378a833 (diff)
Add completion macro infrastructure without logic
-rw-r--r--mingling_core/src/asset/comp.rs32
-rw-r--r--mingling_core/src/asset/comp/flags.rs1
-rw-r--r--mingling_core/src/asset/comp/shell_ctx.rs1
-rw-r--r--mingling_core/src/asset/comp/suggest.rs2
-rw-r--r--mingling_core/src/program.rs8
-rw-r--r--mingling_macros/src/completion.rs132
-rw-r--r--mingling_macros/src/dispatcher_chain.rs38
-rw-r--r--mingling_macros/src/lib.rs67
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 {