diff options
| author | Weicao-CatilGrass <1992414357@qq.com> | 2026-05-09 13:21:01 +0800 |
|---|---|---|
| committer | Weicao-CatilGrass <1992414357@qq.com> | 2026-05-09 13:21:01 +0800 |
| commit | c0a29ccd2cd56e75c2b422b3cd9ca65d76a554da (patch) | |
| tree | 26ce74a8f1138bed899d7896b48f1d5706ab96d4 /mingling_core/src/comp.rs | |
| parent | c08ea7d8474335cadf13a2a7e45ca497fe375d90 (diff) | |
Move `comp` module from `asset` to crate root
Diffstat (limited to 'mingling_core/src/comp.rs')
| -rw-r--r-- | mingling_core/src/comp.rs | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs new file mode 100644 index 0000000..4fb17c7 --- /dev/null +++ b/mingling_core/src/comp.rs @@ -0,0 +1,338 @@ +mod flags; +mod shell_ctx; +mod suggest; + +use std::collections::BTreeSet; +use std::fmt::Display; + +#[doc(hidden)] +pub use flags::*; +#[doc(hidden)] +pub use shell_ctx::*; +#[doc(hidden)] +pub use suggest::*; + +use crate::{ProgramCollect, debug, only_debug, this, trace}; + +#[cfg(not(feature = "dispatch_tree"))] +use crate::exec::match_user_input; + +/// 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; +} + +/// 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>; +} + +/// A helper struct for handling command-line completion logic. +/// +/// This struct provides static methods for executing completions based on +/// the current shell context and rendering the resulting suggestions in a +/// format appropriate for the target shell. +pub struct CompletionHelper; +impl CompletionHelper { + pub fn exec_completion<P>(ctx: &ShellContext) -> Suggest + where + P: ProgramCollect<Enum = P> + Display + PartialEq + 'static, + { + only_debug! { + crate::debug::init_env_logger(); + trace_ctx(ctx); + }; + + let args = ctx.all_words.iter().skip(1).cloned().collect::<Vec<_>>(); + trace!("arguments=\"{}\"", args.join(", ")); + + #[cfg(not(feature = "dispatch_tree"))] + let program = this::<P>(); + + #[cfg(not(feature = "dispatch_tree"))] + let suggest = if let Ok((dispatcher, args)) = match_user_input(program, &args) { + trace!( + "dispatcher matched, dispatcher=\"{}\"", + dispatcher.node().to_string(), + ); + let begin = dispatcher.begin(args); + if let crate::ChainProcess::Ok((any, _)) = begin { + trace!("entry type: {}", any.member_id); + let result = P::do_comp(&any, ctx); + trace!("do_comp result: {:?}", result); + Some(result) + } else { + trace!("begin not Ok"); + None + } + } else { + trace!("no dispatcher matched"); + None + }; + #[cfg(feature = "dispatch_tree")] + let suggest = if let Ok(any) = P::dispatch_args_trie(&args) { + trace!("entry type: {}", any.member_id); + + let dispatcher_not_found = <P::DispatcherNotFound as crate::Groupped<P>>::member_id(); + + if dispatcher_not_found == any.member_id { + trace!("begin not Ok"); + None + } else { + let result = P::do_comp(&any, ctx); + trace!("do_comp result: {:?}", result); + Some(result) + } + } else { + trace!("no dispatcher matched"); + None + }; + + match suggest { + Some(suggest) => { + trace!("using custom completion: {:?}", suggest); + suggest + } + None => { + trace!("using default completion"); + default_completion::<P>(ctx) + } + } + } + + pub fn render_suggest<P>(ctx: ShellContext, suggest: Suggest) + where + P: ProgramCollect<Enum = P> + Display + 'static, + { + trace!("render_suggest called with: {:?}", suggest); + match suggest { + Suggest::FileCompletion => { + trace!("rendering file completion"); + println!("_file_"); + std::process::exit(0); + } + Suggest::Suggest(suggestions) => { + trace!("rendering {} suggestions", suggestions.len()); + match ctx.shell_flag { + ShellFlag::Zsh | ShellFlag::Powershell => { + trace!("using zsh/pwsh format"); + print_suggest_with_description(suggestions) + } + ShellFlag::Fish => { + trace!("using fish format"); + print_suggest_with_description_fish(suggestions) + } + _ => { + trace!("using default format"); + print_suggest(suggestions) + } + } + } + } + } +} + +fn default_completion<P>(ctx: &ShellContext) -> Suggest +where + P: ProgramCollect<Enum = P> + Display + 'static, +{ + let cmd_nodes: Vec<String> = this::<P>() + .get_nodes() + .into_iter() + .filter(|(s, _)| s != "__comp") + .map(|(s, _)| s) + .collect(); + debug!("cmd_nodes: {:?}", cmd_nodes); + + // If the current position is less than 1, do not perform completion + if ctx.word_index < 1 { + debug!("word_index < 1, returning file suggestions"); + return file_suggest(); + }; + + // Get the current input path + debug!( + "input_path before filter: {:?}", + &ctx.all_words.get(1..ctx.word_index).unwrap_or(&[]) + ); + + let input_path: Vec<&str> = ctx + .all_words + .get(1..ctx.word_index) + .unwrap_or(&[]) + .iter() + .filter(|s| !s.is_empty()) + .map(|s| s.as_str()) + .collect(); + debug!("input_path after filter: {:?}", input_path); + + debug!( + "default_completion: input_path = {:?}, word_index = {}, all_words = {:?}", + input_path, ctx.word_index, ctx.all_words + ); + + // Filter command nodes that match the input path + let mut suggestions = Vec::new(); + + // Special case: if input_path is empty, return all first-level commands + if input_path.is_empty() { + for node in cmd_nodes { + let node_parts: Vec<&str> = node.split(' ').collect(); + if !node_parts.is_empty() && !suggestions.contains(&node_parts[0].to_string()) { + suggestions.push(node_parts[0].to_string()); + } + } + } else { + // Get the current word + let current_word = input_path.last().unwrap(); + + // First, handle partial match completion for the current word + // Only perform current word completion when current_word is not empty + if input_path.len() == 1 && !ctx.current_word.is_empty() { + for node in &cmd_nodes { + let node_parts: Vec<&str> = node.split(' ').collect(); + if !node_parts.is_empty() + && node_parts[0].starts_with(current_word) + && !suggestions.contains(&node_parts[0].to_string()) + { + suggestions.push(node_parts[0].to_string()); + } + } + + // If suggestions for the current word are found, return directly + if !suggestions.is_empty() { + suggestions.sort(); + suggestions.dedup(); + debug!( + "default_completion: current word suggestions = {:?}", + suggestions + ); + return suggestions.into(); + } + } + + // Handle next-level command suggestions + for node in cmd_nodes { + let node_parts: Vec<&str> = node.split(' ').collect(); + + debug!("Checking node: '{}', parts: {:?}", node, node_parts); + + // If input path is longer than node parts, skip + if input_path.len() > node_parts.len() { + continue; + } + + // Check if input path matches the beginning of node parts + let mut matches = true; + for i in 0..input_path.len() { + if i >= node_parts.len() { + matches = false; + break; + } + + if i == input_path.len() - 1 { + if !node_parts[i].starts_with(input_path[i]) { + matches = false; + break; + } + } else if input_path[i] != node_parts[i] { + matches = false; + break; + } + } + + if matches && input_path.len() <= node_parts.len() { + if input_path.len() == node_parts.len() && !ctx.current_word.is_empty() { + suggestions.push(node_parts[input_path.len() - 1].to_string()); + } else if input_path.len() < node_parts.len() { + suggestions.push(node_parts[input_path.len()].to_string()); + } + } + } + } + + // Remove duplicates and sort + suggestions.sort(); + suggestions.dedup(); + + debug!("default_completion: suggestions = {:?}", suggestions); + + if suggestions.is_empty() { + file_suggest() + } else { + suggestions.into() + } +} + +fn file_suggest() -> Suggest { + trace!("file_suggest called"); + Suggest::FileCompletion +} + +fn print_suggest(suggestions: BTreeSet<SuggestItem>) { + trace!("print_suggest called with {} items", suggestions.len()); + let mut sorted_suggestions: Vec<SuggestItem> = suggestions.into_iter().collect(); + sorted_suggestions.sort(); + + for suggest in sorted_suggestions { + println!("{}", suggest.suggest()); + } + std::process::exit(0); +} + +fn print_suggest_with_description(suggestions: BTreeSet<SuggestItem>) { + trace!( + "print_suggest_with_description called with {} items", + suggestions.len() + ); + let mut sorted_suggestions: Vec<SuggestItem> = suggestions.into_iter().collect(); + sorted_suggestions.sort(); + + for suggest in sorted_suggestions { + match suggest.description() { + Some(desc) => println!("{}$({})", suggest.suggest(), desc), + None => println!("{}", suggest.suggest()), + } + } + std::process::exit(0); +} + +fn print_suggest_with_description_fish(suggestions: BTreeSet<SuggestItem>) { + trace!( + "print_suggest_with_description_fish called with {} items", + suggestions.len() + ); + let mut sorted_suggestions: Vec<SuggestItem> = suggestions.into_iter().collect(); + sorted_suggestions.sort(); + + for suggest in sorted_suggestions { + match suggest.description() { + Some(desc) => println!("{}\t{}", suggest.suggest(), desc), + None => println!("{}", suggest.suggest()), + } + } + std::process::exit(0); +} + +#[cfg(feature = "debug")] +fn trace_ctx(ctx: &ShellContext) { + trace!("=== SHELL CTX BEGIN ==="); + trace!("command_line={}", ctx.command_line); + trace!("cursor_position={}", ctx.cursor_position); + trace!("current_word={}", ctx.current_word); + trace!("previous_word={}", ctx.previous_word); + trace!("command_name={}", ctx.command_name); + trace!("word_index={}", ctx.word_index); + trace!("all_words={:?}", ctx.all_words); + trace!("shell_flag={:?}", ctx.shell_flag); + trace!("=== SHELL CTX END ==="); +} |
