aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-04 15:00:42 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-04 15:00:42 +0800
commita97f8d32a856ae72d128ed61aebfb20709b7d4b9 (patch)
treebfda5c3e8aab3b4bce6ec50f258dbc9a2b8ef557
parent9a231d8c163414d462a185d955e6887b64704802 (diff)
docs: document completion description supportHEADmain
Update changelog and issue tracker notes for the enhanced `default_completion` behavior.
-rw-r--r--CHANGELOG.md4
-rw-r--r--docs/dev/pages/issues/the-shit-time.md40
-rw-r--r--mingling_core/src/comp.rs95
3 files changed, 114 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 084c491..5104a88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -285,6 +285,10 @@ None
The module is gated behind the `core` feature and re-exported as `mingling::metadata`. This type is designed to work hand-in-hand with the compile-time entry metadata system from item 9, providing a first-party convention metadata for describing entries in generated documentation and help output.
+11. **[`core:comp`]** Enhanced `default_completion` to attach each suggestion's owning entry's `Description` metadata as a completion description. A new helper `entry_description::<P>(node)` resolves a space-separated command node path to its owning entry's member id (via the dispatch trie under `dispatch_tree`, or `match_user_input` + dispatcher `begin` otherwise), then retrieves the `Description` via `ProgramCollect::get_metadata::<Description>`. If found, suggestions for that node are built as `SuggestItem::new_with_desc(token, desc)` instead of plain `SuggestItem::new(token)`.
+
+ The suggestion collection was also reworked: it now uses a `BTreeSet<SuggestItem>` for natural ordering and deduplication (replacing the previous `Vec<String>` + manual `sort()`/`dedup()`), carries both the suggested token and the fully-qualified owner node path used for the description lookup, and returns `Suggest::Suggest(suggestions)` directly. `entry_description` returns `None` for intermediate trie segments that have no entry of their own or entries without a registered `Description`, in which case suggestions fall back to plain `SuggestItem::new(token)`. The final empty-suggestions fallback to `file_suggest()` is unchanged.
+
#### **BREAKING CHANGES** (API CHANGES):
1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`.
diff --git a/docs/dev/pages/issues/the-shit-time.md b/docs/dev/pages/issues/the-shit-time.md
index ece670e..4c25e8f 100644
--- a/docs/dev/pages/issues/the-shit-time.md
+++ b/docs/dev/pages/issues/the-shit-time.md
@@ -48,7 +48,7 @@ fn complete_fallback(_ctx: &ShellContext) -> Suggest {
---
-## Why can't I register descriptions for commands?
+## Why can't I register descriptions for commands? (Solved)
(completion) (dispatcher)
@@ -109,3 +109,41 @@ fn desc_add(_ctx: &ShellContext) -> Suggest {
// Match the corresponding function using enum values inside ThisProgram
gen_program!()
```
+
+Final implementation:
+
+`#[metadata]` registers a `Description` for each entry, and `gen_program!()` collects them. During completion, when a subcommand suggestion is generated, its owning entry's `Description` is resolved via the node path and attached as the suggestion's description text.
+
+```rust
+use mingling::{
+ ShellContext, Suggest,
+ macros::{command, completion, gen_program, metadata, suggest},
+ metadata::Description,
+ setup::picker::BasicProgramSetup,
+};
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.with_dispatcher(CMDCompletion);
+ program.with_dispatcher(CMDThanks);
+ program.exec_and_exit();
+}
+
+#[command]
+pub fn thanks() {
+ println!("thanks!");
+}
+
+#[completion(EntryThanks)]
+pub fn complete_thanks(_ctx: &ShellContext) -> Suggest {
+ suggest! { "alice", "bob" }
+}
+
+#[metadata(EntryThanks)]
+pub fn desc_thanks() -> Description {
+ Description::new("Thanks someone")
+}
+
+gen_program!();
+```
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index edf25a6..a49ea0c 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -23,9 +23,11 @@ pub use shell_ctx::*;
#[doc(hidden)]
pub use suggest::*;
-use crate::{ProgramCollect, debug, only_debug, this, trace};
+use crate::{ProgramCollect, debug, metadata::Description, only_debug, this, trace};
#[cfg(not(feature = "dispatch_tree"))]
+use crate::ChainProcess;
+#[cfg(not(feature = "dispatch_tree"))]
use crate::exec::match_user_input;
/// Trait for implementing completion logic.
@@ -273,6 +275,36 @@ where
})
}
+/// Resolves the `Description` metadata registered for the entry that owns the
+/// given (space-separated) command node path, if any.
+///
+/// The node path is dispatched to obtain the owning entry's member id, and its
+/// `Description` is then retrieved via [`ProgramCollect::get_metadata`]. Returns
+/// `None` when the path does not resolve to a concrete entry (e.g. an
+/// intermediate trie segment that has no entry of its own) or when the entry
+/// has no `Description` registered.
+fn entry_description<P>(node: &str) -> Option<String>
+where
+ P: ProgramCollect<Enum = P> + Display + 'static,
+{
+ let words: Vec<String> = node.split(' ').map(str::to_string).collect();
+
+ #[cfg(feature = "dispatch_tree")]
+ let lazy_member = P::dispatch_args_trie(&words).ok().map(|any| any.member_id);
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let lazy_member = match match_user_input(this::<P>(), &words) {
+ Ok((dispatcher, args)) => match dispatcher.begin(args) {
+ ChainProcess::Ok((any, _)) => Some(any.member_id),
+ _ => None,
+ },
+ Err(_) => None,
+ };
+
+ let member_id = lazy_member?;
+ P::get_metadata::<Description>(member_id).map(String::from)
+}
+
fn default_completion<P>(ctx: &ShellContext) -> Suggest
where
P: ProgramCollect<Enum = P> + Display + 'static,
@@ -321,16 +353,27 @@ where
input_path, ctx.word_index, ctx.all_words
);
- // Filter command nodes that match the input path
- let mut suggestions = Vec::new();
+ // Build a suggestion item for `token`, attaching the owning entry's
+ // `Description` metadata (resolved via `node_path`) when one is available.
+ let make_item = |token: &str, node_path: &str| -> SuggestItem {
+ match entry_description::<P>(node_path) {
+ Some(desc) => SuggestItem::new_with_desc(token.to_string(), desc),
+ None => SuggestItem::new(token.to_string()),
+ }
+ };
+
+ // Track both the suggestion text and the node path used to look up its
+ // description, then deduplicate by suggestion text.
+ let mut suggestions: std::collections::BTreeSet<SuggestItem> =
+ std::collections::BTreeSet::new();
// Special case: if input_path is empty, return all first-level commands
if input_path.is_empty() {
debug!("input_path empty, returning first-level commands");
for node in cmd_nodes {
let node_parts: Vec<&str> = node.split(' ').collect();
- if !node_parts.is_empty() && !suggestions.contains(&node_parts[0].to_string()) {
- suggestions.push(node_parts[0].to_string());
+ if let Some(first) = node_parts.first() {
+ suggestions.insert(make_item(first, first));
}
}
} else {
@@ -343,23 +386,21 @@ where
if input_path.len() == 1 && !ctx.current_word.is_empty() {
for node in &cmd_nodes {
let node_parts: Vec<&str> = node.split(' ').collect();
- if !node_parts.is_empty()
- && node_parts[0].starts_with(current_word)
- && !suggestions.contains(&node_parts[0].to_string())
- {
- suggestions.push(node_parts[0].to_string());
+ let Some(first) = node_parts.first() else {
+ continue;
+ };
+ if first.starts_with(current_word) {
+ suggestions.insert(make_item(first, first));
}
}
// If suggestions for the current word are found, return directly
if !suggestions.is_empty() {
- suggestions.sort();
- suggestions.dedup();
debug!(
"default_completion: current word suggestions = {:?}",
suggestions
);
- return suggestions.into();
+ return Suggest::Suggest(suggestions);
}
}
@@ -397,31 +438,37 @@ where
let last_idx = input_path.len() - 1;
let is_partial = input_path[last_idx] != node_parts[last_idx];
+ // The suggested token and the fully qualified node path that
+ // owns it, used to look up its `Description` metadata.
+ let (token, owner_path) = if input_path.len() == node_parts.len() {
+ // Completing the final token of this node.
+ (node_parts[last_idx], node_parts.join(" "))
+ } else if is_partial {
+ // Completing the current (partial) token in place.
+ (node_parts[last_idx], node_parts[..=last_idx].join(" "))
+ } else {
+ // Advancing to the next level under the matched prefix.
+ let idx = input_path.len();
+ (node_parts[idx], node_parts[..=idx].join(" "))
+ };
+
if input_path.len() == node_parts.len() {
if !ctx.current_word.is_empty() {
- suggestions.push(node_parts[last_idx].to_string());
+ suggestions.insert(make_item(token, &owner_path));
}
} else if input_path.len() < node_parts.len() {
- if is_partial {
- suggestions.push(node_parts[last_idx].to_string());
- } else {
- suggestions.push(node_parts[input_path.len()].to_string());
- }
+ suggestions.insert(make_item(token, &owner_path));
}
}
}
}
- // Remove duplicates and sort
- suggestions.sort();
- suggestions.dedup();
-
debug!("default_completion: suggestions = {:?}", suggestions);
if suggestions.is_empty() {
file_suggest()
} else {
- suggestions.into()
+ Suggest::Suggest(suggestions)
}
}