diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-04-11 18:29:57 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-04-11 18:29:57 +0800 |
| commit | 1eb5306fe6e0e09970b37d267051589d8133a824 (patch) | |
| tree | 67dc49cd20ec213ed674e877268cd69633e68690 | |
| parent | 0b9d890fd9764fa3ec0b749f4b15610e49e3cb8d (diff) | |
Implement basic command completion with shell support
| -rw-r--r-- | mingling_core/src/asset/comp.rs | 249 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/shell_ctx.rs | 13 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 2 | ||||
| -rw-r--r-- | mingling_core/src/program/exec.rs | 9 | ||||
| -rw-r--r-- | mingling_macros/src/suggest.rs | 4 |
5 files changed, 254 insertions, 23 deletions
diff --git a/mingling_core/src/asset/comp.rs b/mingling_core/src/asset/comp.rs index 3c22e12..95efc70 100644 --- a/mingling_core/src/asset/comp.rs +++ b/mingling_core/src/asset/comp.rs @@ -2,6 +2,7 @@ mod flags; mod shell_ctx; mod suggest; +use std::collections::BTreeSet; use std::fmt::Display; #[doc(hidden)] @@ -11,7 +12,7 @@ pub use shell_ctx::*; #[doc(hidden)] pub use suggest::*; -use crate::{ProgramCollect, exec::match_user_input, this}; +use crate::{ProgramCollect, debug, exec::match_user_input, only_debug, this, trace}; /// Trait for implementing completion logic. /// @@ -38,21 +39,43 @@ impl CompletionHelper { where P: ProgramCollect<Enum = P> + Display + 'static, { + only_debug! { + crate::debug::init_env_logger(); + trace_ctx(ctx); + }; + let program = this::<P>(); - let suggest = if let Some((dispatcher, args)) = match_user_input(program).ok() { + let args = ctx.all_words.iter().skip(1).cloned().collect::<Vec<_>>(); + let suggest = if let Some((dispatcher, args)) = match_user_input(program, args).ok() { + trace!( + "dispatcher matched, dispatcher=\"{}\", args={:?}", + dispatcher.node().to_string(), + args + ); let begin = dispatcher.begin(args); if let crate::ChainProcess::Ok((any, _)) = begin { - Some(P::do_comp(&any, ctx)) + 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 }; match suggest { - Some(suggest) => suggest, - None => default_completion(ctx), + Some(suggest) => { + trace!("using custom completion: {:?}", suggest); + suggest + } + None => { + trace!("using default completion"); + default_completion::<P>(ctx) + } } } @@ -60,10 +83,220 @@ impl CompletionHelper { where P: ProgramCollect<Enum = P> + Display + 'static, { - todo!() + 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 => { + trace!("using zsh 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, +{ + try_comp_cmd_nodes::<P>(ctx) +} + +fn try_comp_cmd_nodes<P>(ctx: &ShellContext) -> Suggest +where + P: ProgramCollect<Enum = P> + Display + 'static, +{ + let cmd_nodes: Vec<String> = this::<P>() + .get_nodes() + .into_iter() + .map(|(s, _)| s) + .collect(); + + // If the current position is less than 1, do not perform completion + if ctx.word_index < 1 { + return file_suggest(); + }; + + // Get the current input path + let input_path: Vec<&str> = ctx.all_words[1..ctx.word_index] + .iter() + .filter(|s| !s.is_empty()) + .map(|s| s.as_str()) + .collect(); + + debug!( + "try_comp_cmd_nodes: 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!( + "try_comp_cmd_nodes: 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!("try_comp_cmd_nodes: 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); } -fn default_completion(ctx: &ShellContext) -> Suggest { - todo!() +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 ==="); } diff --git a/mingling_core/src/asset/comp/shell_ctx.rs b/mingling_core/src/asset/comp/shell_ctx.rs index 4771e63..5ab0514 100644 --- a/mingling_core/src/asset/comp/shell_ctx.rs +++ b/mingling_core/src/asset/comp/shell_ctx.rs @@ -74,22 +74,17 @@ impl TryFrom<Vec<String>> for ShellContext { .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, + command_line: command_line.replace('^', "-"), cursor_position, - current_word, - previous_word, - command_name, + current_word: current_word.replace('^', "-"), + previous_word: previous_word.replace('^', "-"), + command_name: command_name.replace('^', "-"), word_index, all_words, shell_flag, diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index dacc1b4..b4124a9 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -55,3 +55,5 @@ pub mod build { #[cfg(feature = "comp")] pub use crate::builds::comp::*; } + +pub mod debug; diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs index 072f4cb..71d73e6 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -21,7 +21,7 @@ where let mut stop_next = false; // Match user input - match match_user_input(&program) { + match match_user_input(&program, program.args.clone()) { Ok((dispatcher, args)) => { // Entry point current = match dispatcher.begin(args) { @@ -75,13 +75,14 @@ where #[allow(clippy::type_complexity)] pub fn match_user_input<C, G>( program: &Program<C, G>, + args: Vec<String>, ) -> Result<(&Box<dyn Dispatcher<G> + Send + Sync>, Vec<String>), ProgramInternalExecuteError> where C: ProgramCollect<Enum = G>, G: Display, { let nodes = program.get_nodes(); - let command = format!("{} ", program.args.join(" ")); + let command = format!("{} ", args.join(" ")); // Find all nodes that match the command prefix let matching_nodes: Vec<&(String, &Box<dyn Dispatcher<G> + Send + Sync>)> = nodes @@ -98,7 +99,7 @@ where 1 => { let matched_prefix = matching_nodes[0]; let prefix_len = matched_prefix.0.split_whitespace().count(); - let trimmed_args: Vec<String> = program.args.iter().skip(prefix_len).cloned().collect(); + let trimmed_args: Vec<String> = args.iter().skip(prefix_len).cloned().collect(); Ok((matched_prefix.1, trimmed_args)) } _ => { @@ -110,7 +111,7 @@ where .unwrap(); let prefix_len = matched_prefix.0.split_whitespace().count(); - let trimmed_args: Vec<String> = program.args.iter().skip(prefix_len).cloned().collect(); + let trimmed_args: Vec<String> = args.iter().skip(prefix_len).cloned().collect(); Ok((matched_prefix.1, trimmed_args)) } } diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/suggest.rs index 886eee0..7354ff0 100644 --- a/mingling_macros/src/suggest.rs +++ b/mingling_macros/src/suggest.rs @@ -58,11 +58,11 @@ pub fn suggest(input: TokenStream) -> TokenStream { let expanded = if items.is_empty() { quote! { - ::mingling::Suggest::default() + ::mingling::Suggest::new() } } else { quote! {{ - let mut suggest = ::mingling::Suggest::default(); + let mut suggest = ::mingling::Suggest::new(); #(suggest.insert(#items);)* suggest }} |
