aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/asset.rs1
-rw-r--r--mingling_core/src/asset/metadata.rs14
-rw-r--r--mingling_core/src/comp.rs117
-rw-r--r--mingling_core/src/lib.rs4
-rw-r--r--mingling_core/src/metadata.rs2
-rw-r--r--mingling_core/src/metadata/description.rs57
-rw-r--r--mingling_core/src/program/collection.rs12
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(