diff options
Diffstat (limited to 'mingling_core')
| -rw-r--r-- | mingling_core/src/comp.rs | 80 | ||||
| -rw-r--r-- | mingling_core/src/comp/suggest.rs | 66 | ||||
| -rw-r--r-- | mingling_core/src/program/collection.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/program/collection/mock.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/program/exec.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/program/hook.rs | 4 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/bash.sh | 43 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/zsh.zsh | 8 |
8 files changed, 174 insertions, 39 deletions
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs index d8dcfbd..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"))] @@ -138,11 +148,10 @@ impl CompletionHelper { debug!("dispatch_args_trie OK, member_id = {:?}", any.member_id); trace!("entry type: {}", any.member_id); - let dispatcher_not_found = - <P::ErrorDispatcherNotFound as crate::Grouped<P>>::member_id(); + let entry_fallback = <P::EntryFallback as crate::Grouped<P>>::member_id(); - if dispatcher_not_found == any.member_id { - debug!("dispatcher_not_found matched"); + if entry_fallback == any.member_id { + debug!("entry_fallback matched"); trace!("begin not Ok"); None } else { @@ -163,8 +172,22 @@ impl CompletionHelper { suggest } None => { - trace!("using default completion"); - default_completion::<P>(ctx) + 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) + } + } } } } @@ -212,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, @@ -238,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/src/program/collection.rs b/mingling_core/src/program/collection.rs index fa062cf..1b4d7dd 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -22,7 +22,7 @@ pub trait ProgramCollect { /// Enum type representing internal IDs for the program type Enum; /// Error type when a dispatcher is not found for the given member - type ErrorDispatcherNotFound: Grouped<Self::Enum>; + type EntryFallback: Grouped<Self::Enum>; /// Error type when a renderer is not found for the given member type ErrorRendererNotFound: Grouped<Self::Enum>; @@ -55,7 +55,7 @@ pub trait ProgramCollect { fn build_renderer_not_found(member_id: Self::Enum) -> AnyOutput<Self::Enum>; /// Build an [`AnyOutput`](./struct.AnyOutput.html) to indicate that a dispatcher was not found - fn build_dispatcher_not_found(args: Vec<String>) -> AnyOutput<Self::Enum>; + fn build_entry_fallback(args: Vec<String>) -> AnyOutput<Self::Enum>; /// Build an [`AnyOutput`](./struct.AnyOutput.html) to indicate that the chain returned an empty result fn build_empty_result() -> AnyOutput<Self::Enum>; diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs index dbe4789..cd2abf5 100644 --- a/mingling_core/src/program/collection/mock.rs +++ b/mingling_core/src/program/collection/mock.rs @@ -34,7 +34,7 @@ unsafe impl Grouped<MockProgramCollect> for MockProgramCollect { impl ProgramCollect for MockProgramCollect { type Enum = MockProgramCollect; - type ErrorDispatcherNotFound = MockProgramCollect; + type EntryFallback = MockProgramCollect; type ErrorRendererNotFound = MockProgramCollect; type ResultEmpty = MockProgramCollect; @@ -54,7 +54,7 @@ impl ProgramCollect for MockProgramCollect { unreachable!() } - fn build_dispatcher_not_found(_args: Vec<String>) -> AnyOutput<Self::Enum> { + fn build_entry_fallback(_args: Vec<String>) -> AnyOutput<Self::Enum> { unreachable!() } diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs index f0322a5..d9b4dd8 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -45,7 +45,7 @@ where } // Current - let mut current = C::build_dispatcher_not_found(vec![]); + let mut current = C::build_entry_fallback(vec![]); // Run hooks control!( @@ -193,7 +193,7 @@ where } Err(ProgramInternalExecuteError::DispatcherNotFound) => { // No matching Dispatcher is found - C::build_dispatcher_not_found(args.to_vec()) + C::build_entry_fallback(args.to_vec()) } Err(e) => return Err(e), }; diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index 7d94a21..50c53d7 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -713,7 +713,7 @@ mod tests { impl ProgramCollect for MockHookEnum { type Enum = MockHookEnum; - type ErrorDispatcherNotFound = MockHookEnum; + type EntryFallback = MockHookEnum; type ErrorRendererNotFound = MockHookEnum; type ResultEmpty = MockHookEnum; @@ -721,7 +721,7 @@ mod tests { unreachable!() } - fn build_dispatcher_not_found(_args: Vec<String>) -> crate::AnyOutput<MockHookEnum> { + fn build_entry_fallback(_args: Vec<String>) -> crate::AnyOutput<MockHookEnum> { unreachable!() } 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") |
