aboutsummaryrefslogtreecommitdiff
path: root/mingling_core
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core')
-rw-r--r--mingling_core/src/comp.rs75
-rw-r--r--mingling_core/src/comp/suggest.rs66
-rw-r--r--mingling_core/tmpls/comps/bash.sh43
-rw-r--r--mingling_core/tmpls/comps/zsh.zsh8
4 files changed, 163 insertions, 29 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
diff --git a/mingling_core/tmpls/comps/bash.sh b/mingling_core/tmpls/comps/bash.sh
index 1af4f6c..edec28d 100644
--- a/mingling_core/tmpls/comps/bash.sh
+++ b/mingling_core/tmpls/comps/bash.sh
@@ -1,22 +1,31 @@
#!/usr/bin/env bash
_<<<bin_name>>>_bash_completion() {
- local cur="${COMP_WORDS[COMP_CWORD]}"
+ local line="${COMP_LINE:0:COMP_POINT}"
+ local cur="${line##* }"
local prev=""
- [ $COMP_CWORD -gt 0 ] && prev="${COMP_WORDS[COMP_CWORD-1]}"
+ local word_index=1
- local word_index=$((COMP_CWORD + 1))
+ local before="${line:0:$(( ${#line} - ${#cur} ))}"
+ local -a before_words
+ if [[ -n "$before" ]]; then
+ read -ra before_words <<< "$before"
+ word_index=$(( ${#before_words[@]} + 1 ))
+ if [[ $word_index -gt 1 ]]; then
+ prev="${before_words[${#before_words[@]}-1]}"
+ fi
+ fi
local args=()
- args+=(-f="${COMP_LINE//-/^}")
- args+=(-C="$COMP_POINT")
- args+=(-w="${cur//-/^}")
- args+=(-p="${prev//-/^}")
- args+=(-c="${COMP_WORDS[0]//-/^}")
- args+=(-i="$word_index")
- args+=(-F="bash")
+ args+=(-f "${COMP_LINE//-/^}")
+ args+=(-C "$COMP_POINT")
+ args+=(-w "${cur//-/^}")
+ args+=(-p "${prev//-/^}")
+ args+=(-c "${COMP_WORDS[0]//-/^}")
+ args+=(-i "$word_index")
+ args+=(-F "bash")
for word in "${COMP_WORDS[@]}"; do
- args+=(-a="${word//-/^}")
+ args+=(-a "${word//-/^}")
done
local suggestions
@@ -36,7 +45,17 @@ _<<<bin_name>>>_bash_completion() {
[ -z "$cur" ] || [[ "$suggestion" == "$cur"* ]] && filtered+=("$suggestion")
done
- [ ${#filtered[@]} -gt 0 ] && COMPREPLY=("${filtered[@]}")
+ if [ ${#filtered[@]} -gt 0 ]; then
+ COMPREPLY=("${filtered[@]}")
+ if [[ "$cur" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
+ local colon_prefix="${cur%"${cur##*:}"}"
+ local -a ltrimmed=()
+ for suggestion in "${COMPREPLY[@]}"; do
+ ltrimmed+=("${suggestion#"$colon_prefix"}")
+ done
+ COMPREPLY=("${ltrimmed[@]}")
+ fi
+ fi
return
fi
fi
diff --git a/mingling_core/tmpls/comps/zsh.zsh b/mingling_core/tmpls/comps/zsh.zsh
index c1c18bb..f665133 100644
--- a/mingling_core/tmpls/comps/zsh.zsh
+++ b/mingling_core/tmpls/comps/zsh.zsh
@@ -38,9 +38,9 @@ _<<<bin_name>>>_completion() {
local -a parsed_completions
for item in "${completions[@]}"; do
if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then
- parsed_completions+=("${match[1]}:${match[2]}")
+ parsed_completions+=("${match[1]//:/\\:}:${match[2]}")
else
- parsed_completions+=("$item")
+ parsed_completions+=("${item//:/\\:}")
fi
done
@@ -48,8 +48,8 @@ _<<<bin_name>>>_completion() {
_describe '<<<bin_name>>> commands' parsed_completions
else
local -a simple_completions
- for item in "${parsed_completions[@]}"; do
- if [[ "$item" =~ '^([^:]+):(.+)$' ]]; then
+ for item in "${completions[@]}"; do
+ if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then
simple_completions+=("${match[1]}")
else
simple_completions+=("$item")