aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/comp.rs75
-rw-r--r--mingling_core/src/comp/suggest.rs66
2 files changed, 128 insertions, 13 deletions
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index 021793b..5f851ca 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -107,7 +107,17 @@ impl CompletionHelper {
trace_ctx(ctx);
};
- let args = ctx.all_words.iter().skip(1).cloned().collect::<Vec<_>>();
+ // Everything before the first argument that matches a command node
+ // is treated as global parameters (flags and their values), which do
+ // not participate in command tree matching. The dispatch path starts
+ // at that first match, enabling `prog [PARAM]... <subcommand>`
+ // style invocations.
+ let all_args = ctx.all_words.iter().skip(1).cloned().collect::<Vec<_>>();
+ let first_cmd_match = first_command_arg_index::<P>(&all_args);
+ let args = match first_cmd_match {
+ Some(start) => all_args[start..].to_vec(),
+ None => Vec::new(),
+ };
trace!("arguments=\"{}\"", args.join(", "));
#[cfg(not(feature = "dispatch_tree"))]
@@ -162,10 +172,22 @@ impl CompletionHelper {
suggest
}
None => {
- trace!("using default completion");
- let fallback = P::do_comp(&P::build_entry_fallback(vec![]), ctx);
- let default = default_completion::<P>(ctx);
- fallback.combine(default)
+ if first_cmd_match.is_some() {
+ // A command node has been matched: the global
+ // EntryFallback must not run afterwards, only the
+ // command path is completed.
+ trace!("command node matched, skipping EntryFallback");
+ default_completion::<P>(ctx)
+ } else {
+ trace!("using default completion");
+ let fallback = P::do_comp(&P::build_entry_fallback(vec![]), ctx);
+ let default = default_completion::<P>(ctx);
+ if fallback == Suggest::FileCompletion {
+ default
+ } else {
+ fallback.combine(default)
+ }
+ }
}
}
}
@@ -213,6 +235,30 @@ impl CompletionHelper {
}
}
+/// Finds the index of the first argument that matches the head of a
+/// registered command node.
+///
+/// Everything before this index is treated as global parameters (flags and
+/// their values), which do not participate in command tree matching. This
+/// allows `prog [PARAM]... <subcommand>` style invocations to resolve the
+/// subcommand, while a "broken" path such as `prog -v hello -a someone`
+/// still fails to match the `hello someone` node.
+fn first_command_arg_index<P>(args: &[String]) -> Option<usize>
+where
+ P: ProgramCollect<Enum = P> + Display + 'static,
+{
+ let cmd_heads: Vec<String> = this::<P>()
+ .get_nodes()
+ .into_iter()
+ .filter(|(s, _)| !s.starts_with('_'))
+ .map(|(s, _)| s.split(' ').next().unwrap_or("").to_string())
+ .collect();
+
+ args.iter().position(|arg| {
+ !arg.is_empty() && cmd_heads.iter().any(|head| head.starts_with(arg.as_str()))
+ })
+}
+
fn default_completion<P>(ctx: &ShellContext) -> Suggest
where
P: ProgramCollect<Enum = P> + Display + 'static,
@@ -239,14 +285,17 @@ where
&ctx.all_words.get(1..input_end).unwrap_or(&[])
);
- let input_path: Vec<&str> = ctx
- .all_words
- .get(1..input_end)
- .unwrap_or(&[])
- .iter()
- .filter(|s| !s.is_empty())
- .map(std::string::String::as_str)
- .collect();
+ // Skip global parameters (arguments before the first command node match)
+ // when resolving the command path, so `prog [PARAM]... <subcommand>`
+ // style invocations suggest the subcommand.
+ let input_slice = ctx.all_words.get(1..input_end).unwrap_or(&[]);
+ let input_path: Vec<&str> = match first_command_arg_index::<P>(input_slice) {
+ Some(start) => input_slice[start..]
+ .iter()
+ .map(std::string::String::as_str)
+ .collect(),
+ None => Vec::new(),
+ };
debug!(
"input_path={:?}, current_word='{}'",
input_path, ctx.current_word
diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs
index dda5026..804d622 100644
--- a/mingling_core/src/comp/suggest.rs
+++ b/mingling_core/src/comp/suggest.rs
@@ -92,6 +92,72 @@ impl Suggest {
self.insert(SuggestItem::WithDescription(item, desc_str.clone()));
}
}
+
+ /// Adds a prefix to every suggestion in the `Suggest` set.
+ ///
+ /// This method takes the current `Suggest` value and prepends the given
+ /// prefix to the suggestion text of each item. If the `Suggest` value is
+ /// [`Suggest::FileCompletion`], it is returned unchanged.
+ ///
+ /// # Arguments
+ ///
+ /// * `prefix` — The string to prepend to each suggestion. Must implement
+ /// `Into<String>`.
+ ///
+ /// # Returns
+ ///
+ /// A new `Suggest` value where each item's suggestion text is prefixed
+ /// with the given string. For example, `["foo", "bar"]` with prefix `"--"`
+ /// becomes `["--foo", "--bar"]`.
+ pub fn add_prefix(self, prefix: impl Into<String>) -> Suggest {
+ let suggest = match self {
+ Suggest::Suggest(s) => s,
+ Suggest::FileCompletion => return Suggest::FileCompletion,
+ };
+ let prefix = prefix.into();
+ let prefixed = suggest
+ .into_iter()
+ .map(|item| {
+ let mut new_item = item;
+ new_item.set_suggest(format!("{}{}", prefix, new_item.suggest()));
+ new_item
+ })
+ .collect();
+ Suggest::Suggest(prefixed)
+ }
+
+ /// Appends a suffix to every suggestion in the `Suggest` set.
+ ///
+ /// This method takes the current `Suggest` value and appends the given
+ /// suffix to the suggestion text of each item. If the `Suggest` value is
+ /// [`Suggest::FileCompletion`], it is returned unchanged.
+ ///
+ /// # Arguments
+ ///
+ /// * `suffix` — The string to append to each suggestion. Must implement
+ /// `Into<String>`.
+ ///
+ /// # Returns
+ ///
+ /// A new `Suggest` value where each item's suggestion text is suffixed
+ /// with the given string. For example, `["foo", "bar"]` with suffix `"="`
+ /// becomes `["foo=", "bar="]`.
+ pub fn add_suffix(self, suffix: impl Into<String>) -> Suggest {
+ let suggest = match self {
+ Suggest::Suggest(s) => s,
+ Suggest::FileCompletion => return Suggest::FileCompletion,
+ };
+ let suffix = suffix.into();
+ let suffixed = suggest
+ .into_iter()
+ .map(|item| {
+ let mut new_item = item;
+ new_item.set_suggest(format!("{}{}", new_item.suggest(), suffix));
+ new_item
+ })
+ .collect();
+ Suggest::Suggest(suffixed)
+ }
}
impl<T> From<T> for Suggest