diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-04 15:00:42 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-04 15:00:42 +0800 |
| commit | a97f8d32a856ae72d128ed61aebfb20709b7d4b9 (patch) | |
| tree | bfda5c3e8aab3b4bce6ec50f258dbc9a2b8ef557 | |
| parent | 9a231d8c163414d462a185d955e6887b64704802 (diff) | |
Update changelog and issue tracker notes for the enhanced
`default_completion` behavior.
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | docs/dev/pages/issues/the-shit-time.md | 40 | ||||
| -rw-r--r-- | mingling_core/src/comp.rs | 95 |
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) } } |
