From eb54c560b5832ea4ca5129e13805be3c338ad2c9 Mon Sep 17 00:00:00 2001
From: 魏曹先生 <1992414357@qq.com>
Date: Sun, 31 May 2026 17:10:34 +0800
Subject: Fix trailing space and partial match bugs in default completion
---
CHANGELOG.md | 4 +++-
mingling_core/src/comp.rs | 33 +++++++++++++++++++++++++++------
2 files changed, 30 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ec5cce..17e356c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,9 @@
#### Fixes:
-None
+1. **\[core:comp\]** Fixed `default_completion` incorrectly handling multi-level subcommand suggestions when the cursor is after a trailing space. `all_words.get(1..word_index)` could go out of bounds because Zsh's `$CURRENT` (`word_index`) may exceed `all_words.len()` when trailing whitespace is present. The range end is now capped with `.min(all_words.len())`.
+
+2. **\[core:comp\]** Fixed `default_completion` jumping to the next subcommand level on partial input (e.g. typing `b` for `bind` would skip `bind` and directly suggest third-level commands `add`/`ls`/`rm`). Now if the last input word is only a partial match (`starts_with` but not equal), the current-level word is suggested instead of skipping ahead.
#### Optimizations:
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index 9d84557..8d55c5d 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -45,7 +45,7 @@ pub struct CompletionHelper;
impl CompletionHelper {
pub fn exec_completion
(ctx: &ShellContext) -> Suggest
where
- P: ProgramCollect + Display + PartialEq + 'static,
+ P: ProgramCollect + Display + PartialEq + 'static + std::fmt::Debug,
{
only_debug! {
crate::debug::init_env_logger();
@@ -80,20 +80,24 @@ impl CompletionHelper {
};
#[cfg(feature = "dispatch_tree")]
let suggest = if let Ok(any) = P::dispatch_args_trie(&args) {
+ debug!("dispatch_args_trie OK, member_id = {:?}", any.member_id);
trace!("entry type: {}", any.member_id);
let dispatcher_not_found =
>::member_id();
if dispatcher_not_found == any.member_id {
+ debug!("dispatcher_not_found matched");
trace!("begin not Ok");
None
} else {
let result = P::do_comp(&any, ctx);
+ debug!("do_comp result: {:?}", result);
trace!("do_comp result: {:?}", result);
Some(result)
}
} else {
+ debug!("dispatch_args_trie failed, args = {:?}", args);
trace!("no dispatcher matched");
None
};
@@ -161,19 +165,25 @@ where
};
// Get the current input path
+ let input_end = ctx.word_index.min(ctx.all_words.len());
+
debug!(
"input_path before filter: {:?}",
- &ctx.all_words.get(1..ctx.word_index).unwrap_or(&[])
+ &ctx.all_words.get(1..input_end).unwrap_or(&[])
);
let input_path: Vec<&str> = ctx
.all_words
- .get(1..ctx.word_index)
+ .get(1..input_end)
.unwrap_or(&[])
.iter()
.filter(|s| !s.is_empty())
.map(|s| s.as_str())
.collect();
+ debug!(
+ "input_path={:?}, current_word='{}'",
+ input_path, ctx.current_word
+ );
debug!("input_path after filter: {:?}", input_path);
debug!(
@@ -186,6 +196,7 @@ where
// Special case: if input_path is empty, return all first-level commands
if input_path.is_empty() {
+ debug!("input_path empty, returning first-level commands");
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()) {
@@ -193,6 +204,7 @@ where
}
}
} else {
+ debug!("input_path NOT empty, doing next-level suggestions");
// Get the current word
let current_word = input_path.last().unwrap();
@@ -252,10 +264,19 @@ where
}
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());
+ let last_idx = input_path.len() - 1;
+ let is_partial = input_path[last_idx] != node_parts[last_idx];
+
+ if input_path.len() == node_parts.len() {
+ if !ctx.current_word.is_empty() {
+ suggestions.push(node_parts[last_idx].to_string());
+ }
} else if input_path.len() < node_parts.len() {
- suggestions.push(node_parts[input_path.len()].to_string());
+ if is_partial {
+ suggestions.push(node_parts[last_idx].to_string());
+ } else {
+ suggestions.push(node_parts[input_path.len()].to_string());
+ }
}
}
}
--
cgit