aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/comp.rs80
-rw-r--r--mingling_core/src/comp/shell_ctx.rs10
-rw-r--r--mingling_core/src/comp/suggest.rs66
-rw-r--r--mingling_core/src/program/collection.rs4
-rw-r--r--mingling_core/src/program/collection/mock.rs4
-rw-r--r--mingling_core/src/program/exec.rs4
-rw-r--r--mingling_core/src/program/hook.rs4
7 files changed, 147 insertions, 25 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/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
index 8efbc9d..734b5d2 100644
--- a/mingling_core/src/comp/shell_ctx.rs
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -50,8 +50,14 @@ impl TryFrom<Vec<String>> for ShellContext {
let word_index = special_argument!(args, "-i")
.and_then(|s| s.parse().ok())
.unwrap_or_default();
- let shell_flag = special_argument!(args, "-F")
- .map_or(ShellFlag::Other("unknown".to_string()), ShellFlag::from);
+ // Distinguish "-F absent" (unknown shell) from "-F present without a value" (empty shell)
+ let has_shell_flag = args.iter().any(|arg| arg == "-F");
+ let shell_flag = if has_shell_flag {
+ special_argument!(args, "-F")
+ .map_or_else(|| ShellFlag::Other(String::new()), ShellFlag::from)
+ } else {
+ ShellFlag::Other("unknown".to_string())
+ };
let all_words = command_line
.split_whitespace()
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!()
}