diff options
Diffstat (limited to 'mingling_core')
| -rw-r--r-- | mingling_core/src/asset.rs | 1 | ||||
| -rw-r--r-- | mingling_core/src/asset/metadata.rs | 14 | ||||
| -rw-r--r-- | mingling_core/src/comp.rs | 117 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/metadata.rs | 2 | ||||
| -rw-r--r-- | mingling_core/src/metadata/description.rs | 57 | ||||
| -rw-r--r-- | mingling_core/src/program/collection.rs | 12 |
7 files changed, 181 insertions, 26 deletions
diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index 8c709ac..fc1c81b 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -5,6 +5,7 @@ pub(crate) mod enum_tag; pub(crate) mod global_resource; pub(crate) mod help; pub(crate) mod lazy_resource; +pub(crate) mod metadata; pub(crate) mod node; pub(crate) mod renderer; pub(crate) mod routable; diff --git a/mingling_core/src/asset/metadata.rs b/mingling_core/src/asset/metadata.rs new file mode 100644 index 0000000..996b34c --- /dev/null +++ b/mingling_core/src/asset/metadata.rs @@ -0,0 +1,14 @@ +/// Provides metadata for an Entry. +/// +/// Any type can be attached to an Entry as metadata, allowing the program to +/// carry compile-time-typed, arbitrary description data alongside each +/// registered entry. The [`Metadata`] trait bridges an Entry type (`Self`) to +/// an arbitrary metadata type `B`. +/// +/// It is recommended to use the `#[metadata(Entry)]` attribute macro from +/// [mingling_macros](https://crates.io/crates/mingling_macros) to implement this +/// trait and register the entry via `register_metadata!`. +pub trait Metadata<B> { + /// Initializes and returns the metadata value of type `B` for this entry. + fn init_metadata() -> B; +} diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs index 2317f06..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. @@ -168,8 +170,22 @@ impl CompletionHelper { match suggest { Some(suggest) => { + // A concrete entry was dispatched. Merge the entry's own + // completion with the default subcommand suggestions so that, + // e.g. `thanks <tab>`, suggests both the leaf nodes (`bob`, + // `alice`) and the `thanks` entry's own completion. trace!("using custom completion: {:?}", suggest); - suggest + let default = default_completion::<P>(ctx); + if suggest == Suggest::FileCompletion { + trace!( + "custom completion is FileCompletion, using default: {:?}", + default + ); + default + } else { + trace!("combining custom completion with default"); + suggest.combine(default) + } } None => { if first_cmd_match.is_some() { @@ -182,7 +198,11 @@ impl CompletionHelper { 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 fallback == Suggest::FileCompletion { + default + } else { + fallback.combine(default) + } } } } @@ -255,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, @@ -303,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 { @@ -325,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); } } @@ -379,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) } } diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index cd42842..3db3d31 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -70,6 +70,7 @@ pub use crate::asset::enum_tag::*; pub use crate::asset::global_resource::*; pub use crate::asset::help::*; pub use crate::asset::lazy_resource::*; +pub use crate::asset::metadata::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; pub use crate::asset::routable::*; @@ -102,3 +103,6 @@ mod private; pub mod __private { pub use crate::private::*; } + +/// Mingling's convention metadatas, which can be bound to types using `#[metadata]`, to provide identification for types +pub mod metadata; diff --git a/mingling_core/src/metadata.rs b/mingling_core/src/metadata.rs new file mode 100644 index 0000000..329576c --- /dev/null +++ b/mingling_core/src/metadata.rs @@ -0,0 +1,2 @@ +mod description; +pub use description::*; diff --git a/mingling_core/src/metadata/description.rs b/mingling_core/src/metadata/description.rs new file mode 100644 index 0000000..48bf095 --- /dev/null +++ b/mingling_core/src/metadata/description.rs @@ -0,0 +1,57 @@ +/// Provides a description for any Grouped type. +pub struct Description { + desc: String, +} + +impl Description { + /// Creates a new `Description` instance. + pub fn new<S: Into<String>>(desc: S) -> Self { + Self { desc: desc.into() } + } +} + +impl From<String> for Description { + fn from(desc: String) -> Self { + Self { desc } + } +} + +impl From<&str> for Description { + fn from(desc: &str) -> Self { + Self { + desc: desc.to_string(), + } + } +} + +impl From<Description> for String { + fn from(desc: Description) -> Self { + desc.desc + } +} + +impl From<&Description> for String { + fn from(desc: &Description) -> Self { + desc.desc.clone() + } +} + +impl std::ops::Deref for Description { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.desc + } +} + +impl std::ops::DerefMut for Description { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.desc + } +} + +impl std::fmt::Display for Description { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.desc) + } +} diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs index 1b4d7dd..5b1152a 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -66,6 +66,18 @@ pub trait ProgramCollect { /// Render help for Entry fn render_help(any: AnyOutput<Self::Enum>) -> RenderResult; + /// Retrieves compile-time registered metadata of type `T` for the given + /// enum member, if any was registered via `#[metadata(Entry)]`. + /// + /// Returns `None` when no metadata of type `T` has been registered for the + /// provided member, or when the requested `T` does not match the registered + /// metadata type. The concrete implementation of this method is generated + /// by the `gen_program!` macro. + fn get_metadata<T: 'static>(member_id: Self::Enum) -> Option<T> { + let _ = member_id; + None + } + /// Find a matching chain to continue execution based on the input [AnyOutput](./struct.AnyOutput.html), returning a new [AnyOutput](./struct.AnyOutput.html) #[cfg(feature = "async")] fn do_chain( |
