aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mingling_core/src/asset/comp.rs249
-rw-r--r--mingling_core/src/asset/comp/shell_ctx.rs13
-rw-r--r--mingling_core/src/lib.rs2
-rw-r--r--mingling_core/src/program/exec.rs9
-rw-r--r--mingling_macros/src/suggest.rs4
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
}}