aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.run/src/bin/install-mling.ps12
-rwxr-xr-x[-rw-r--r--].run/src/bin/install-mling.sh2
-rw-r--r--CHANGELOG.md20
-rw-r--r--arg_picker/src/arg.rs4
-rw-r--r--docs/dev/pages/issues/the-shit-time.md2
-rw-r--r--mingling_cli/src/linter/mlint_report.rs12
-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
11 files changed, 196 insertions, 39 deletions
diff --git a/.gitignore b/.gitignore
index 7d58ad3..aeaee81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ docs/cov-test/
__*.md
__*/
__*.py
+__*.rs
# Fuck
nul
diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1
index bebe9ff..7fba62e 100644
--- a/.run/src/bin/install-mling.ps1
+++ b/.run/src/bin/install-mling.ps1
@@ -1,4 +1,4 @@
-cargo install --path mling
+cargo install --path mingling_cli
New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null
# Copy all files containing _comp from the debug directory
diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh
index 5f2ee7a..5b5e7b2 100644..100755
--- a/.run/src/bin/install-mling.sh
+++ b/.run/src/bin/install-mling.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-cargo install --path mling
+cargo install --path mingling_cli
mkdir -p .temp/comp
cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acb3a1d..aa80b79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,7 +57,25 @@ None
#### Fixes:
-None
+1. **[`comps:zsh`]** Fixed zsh completion script to properly escape colons in completion descriptions. The zsh completion script generated for the `zsh` output format now escapes colon characters in completion items (`${item//:/\\:}`) and description parts (`${match[1]//:/\\:}`) so that descriptions containing colons don't break the `_describe` command's parsing. Additionally, fixed the simple-completions branch to iterate over the original `completions` array (with the colon-escaped format matching) rather than the already-parsed `parsed_completions` array, correctly extracting the completion item when no description is present.
+
+2. **[`comps:bash`]** Reworked the bash completion script template to fix word-index tracking when the cursor is in the middle of a word and to add colon-ltrimming for completion descriptions. The script now computes the current word (`cur`) using `COMP_LINE` truncated to `COMP_POINT` (taking the last whitespace-delimited token) rather than relying on `COMP_WORDS[COMP_CWORD]`, which fails when the cursor is in the middle of a word. The word index is derived from the count of words preceding the current cursor position (`before_words`), and `prev` is the last word in that preceding set. The option flags to the underlying completion engine are now passed as `-f value`/`-C value`/etc. (space-separated) instead of `-f=value`/`-C=value`/etc. (equals-separated), since the engine expects space-delimited argument pairs. Additionally, when the current word contains a colon and `COMP_WORDBREAKS` includes `:`, the completion items are trimmed of the colon prefix before being inserted into `COMPREPLY` — this prevents completions like `foo:bar` from being double-prefixed with the literal `foo:bar` when bash would otherwise append the full word.
+
+3. **[`core:comp`]** Fixed the completion engine to correctly resolve command nodes when global parameters (flags and their values) precede the subcommand. Previously, the engine matched the command tree starting from the first argument after the program name, treating every leading argument as part of the command path — so `prog [PARAM]... <subcommand>` style invocations would fail to match any node. Now:
+
+ - A new helper `first_command_arg_index::<P>(args)` scans the argument list and returns the index of the first argument that matches the head of a registered command node (excluding node names starting with `_`). Everything before that index is treated as global parameters and skipped during command tree matching.
+ - In `CompletionHelper::complete`, the dispatch args are sliced to start at the first command-node match (`all_args[start..]`); if no node match is found, the args are empty (`Vec::new()`).
+ - In `default_completion`, the input path resolution skips the leading global-parameter arguments the same way, so `prog -v hello` correctly suggests the `hello` node even though `-v` precedes it.
+ - In the unmatched-dispatcher branch: when a command node _has_ been matched (`first_cmd_match.is_some()`), the `EntryFallback` handler is **skipped** and only `default_completion` runs, since global parameters do not warrant invoking the fallback. When no node was matched, the previous behavior is retained (fallback combined with default completion).
+
+ This enables `prog [PARAM]... <subcommand>` style invocations to resolve the subcommand correctly, while a "broken" path such as `prog -v hello -a someone` still fails to match a `hello someone` node (since `-a someone` lies _after_ the first matched node and participates normally).
+
+4. **[`core:comp`]** Added `add_prefix()` and `add_suffix()` methods to `Suggest` for batch-transforming suggestion text:
+
+ - **`add_prefix(self, prefix: impl Into<String>) -> Suggest`** — Takes the current `Suggest` value and prepends the given prefix to the suggestion text of every item. If the `Suggest` value is `Suggest::FileCompletion`, it is returned unchanged. For example, `["foo", "bar"]` with prefix `"--"` becomes `["--foo", "--bar"]`.
+ - **`add_suffix(self, suffix: impl Into<String>) -> Suggest`** — Takes the current `Suggest` value and appends the given suffix to the suggestion text of every item. If the `Suggest` value is `Suggest::FileCompletion`, it is returned unchanged. For example, `["foo", "bar"]` with suffix `"="` becomes `["foo=", "bar="]`.
+
+ Both methods consume the original `Suggest` value and return a new one, enabling ergonomic chaining with the existing `combine()` method for transforming completion suggestion sets.
#### Optimizations:
diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs
index 0b5176d..32824ea 100644
--- a/arg_picker/src/arg.rs
+++ b/arg_picker/src/arg.rs
@@ -1,4 +1,4 @@
-use crate::{Pickable, PickerArgInfo, SinglePickable, parselib::ParserStyle};
+use crate::{Pickable, PickerArgInfo, parselib::ParserStyle};
use std::marker::PhantomData;
/// Represents a constraint definition for a parameter selection.
@@ -166,7 +166,7 @@ where
impl<'a, Type> From<PickerArg<'a, Type>> for Vec<String>
where
- Type: SinglePickable,
+ Type: Pickable<'a>,
{
fn from(value: PickerArg<'a, Type>) -> Self {
let mut result = Vec::new();
diff --git a/docs/dev/pages/issues/the-shit-time.md b/docs/dev/pages/issues/the-shit-time.md
index b23854e..ece670e 100644
--- a/docs/dev/pages/issues/the-shit-time.md
+++ b/docs/dev/pages/issues/the-shit-time.md
@@ -34,7 +34,7 @@ fn complete(ctx: &ShellContext) -> Suggest {
// ...
}
```
-
+
Final implementation:
By adding support for `EntryFallback` (formerly `ErrorDispatcherNotFound`) to the Completion system, `EntryFallback` can now be used as a completion entry point when no subcommand is matched:
diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs
index 0472056..b594b9d 100644
--- a/mingling_cli/src/linter/mlint_report.rs
+++ b/mingling_cli/src/linter/mlint_report.rs
@@ -10,7 +10,7 @@ use cargo_metadata::{Message, PackageId};
use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING};
use annotate_snippets::{AnnotationKind, Group, Patch, Renderer, Snippet};
use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer};
-use mingling::{AnyOutput, ProgramCollect, Routable};
+use mingling::{RendererInvoker, Routable};
use crate::Next;
use crate::metadata::setup::ResUsingJson;
@@ -437,9 +437,13 @@ pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) {
}
#[renderer(buffer)]
-pub fn render_lint_reports_json(reports: ResultLintReportsJson) {
+pub fn render_lint_reports_json(
+ reports: ResultLintReportsJson,
+ message_renderer: &RendererInvoker<Message>,
+) {
for report in reports.inner {
- // DIRTY: Dispatch to the Message renderer using AnyOutput to obtain the render result and append it to the Buffer
- r_append!(|| { crate::ThisProgram::render(AnyOutput::new(report.to_compiler_message())) });
+ let message = report.to_compiler_message();
+ let result = message_renderer.invoke(message);
+ r_append!(result);
}
}
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")