aboutsummaryrefslogtreecommitdiff
path: root/mingling_core
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core')
-rw-r--r--mingling_core/src/asset.rs1
-rw-r--r--mingling_core/src/asset/metadata.rs14
-rw-r--r--mingling_core/src/build.rs19
-rw-r--r--mingling_core/src/build/pathf.rs2
-rw-r--r--mingling_core/src/comp.rs191
-rw-r--r--mingling_core/src/comp/flags.rs2
-rw-r--r--mingling_core/src/comp/shell_ctx.rs50
-rw-r--r--mingling_core/src/comp/suggest.rs102
-rw-r--r--mingling_core/src/docs/build.md57
-rw-r--r--mingling_core/src/lib.rs19
-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.rs18
-rw-r--r--mingling_core/src/program/collection/mock.rs4
-rw-r--r--mingling_core/src/program/exec.rs4
-rw-r--r--mingling_core/src/program/hook.rs4
-rw-r--r--mingling_core/src/renderer/render_result.rs2
-rw-r--r--mingling_core/tests/test-all/Cargo.toml2
-rw-r--r--mingling_core/tests/test-repl/Cargo.toml2
-rw-r--r--mingling_core/tmpls/comps/bash.sh43
-rw-r--r--mingling_core/tmpls/comps/fish.fish8
-rw-r--r--mingling_core/tmpls/comps/zsh.zsh8
22 files changed, 484 insertions, 127 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/build.rs b/mingling_core/src/build.rs
index 7918f3a..213d529 100644
--- a/mingling_core/src/build.rs
+++ b/mingling_core/src/build.rs
@@ -3,11 +3,26 @@
mod comp;
#[cfg(feature = "comp")]
-pub use comp::*;
+mod comp_re_export {
+ pub use super::comp::build_comp_script;
+ pub use super::comp::build_comp_script_to;
+ pub use super::comp::build_comp_script_to_file;
+ pub use super::comp::build_comp_scripts;
+}
+
+#[cfg(feature = "comp")]
+pub use comp_re_export::*;
#[doc(hidden)]
#[cfg(feature = "pathf")]
mod pathf;
#[cfg(feature = "pathf")]
-pub use pathf::*;
+mod pathf_re_export {
+ pub use super::pathf::analyze;
+ pub use super::pathf::analyze_and_build_type_mapping;
+ pub use super::pathf::analyze_and_build_type_mapping_for;
+}
+
+#[cfg(feature = "pathf")]
+pub use pathf_re_export::*;
diff --git a/mingling_core/src/build/pathf.rs b/mingling_core/src/build/pathf.rs
index d8d4698..7f75c6b 100644
--- a/mingling_core/src/build/pathf.rs
+++ b/mingling_core/src/build/pathf.rs
@@ -1,3 +1,5 @@
+#![allow(unused_imports)]
+
pub use mingling_pathf::config::*;
pub use mingling_pathf::module_pathf::*;
pub use mingling_pathf::pattern_analyzer::*;
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index d8dcfbd..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.
@@ -107,7 +109,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"))]
@@ -138,11 +150,10 @@ impl CompletionHelper {
debug!("dispatch_args_trie OK, member_id = {:?}", any.member_id);
trace!("entry type: {}", any.member_id);
- let dispatcher_not_found =
- <P::ErrorDispatcherNotFound as crate::Grouped<P>>::member_id();
+ let entry_fallback = <P::EntryFallback as crate::Grouped<P>>::member_id();
- if dispatcher_not_found == any.member_id {
- debug!("dispatcher_not_found matched");
+ if entry_fallback == any.member_id {
+ debug!("entry_fallback matched");
trace!("begin not Ok");
None
} else {
@@ -159,12 +170,40 @@ 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 => {
- trace!("using default completion");
- default_completion::<P>(ctx)
+ 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)
+ }
+ }
}
}
}
@@ -212,6 +251,60 @@ 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()))
+ })
+}
+
+/// 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,
@@ -238,14 +331,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
@@ -257,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 {
@@ -279,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);
}
}
@@ -333,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/comp/flags.rs b/mingling_core/src/comp/flags.rs
index 9a88d2e..8aecf1b 100644
--- a/mingling_core/src/comp/flags.rs
+++ b/mingling_core/src/comp/flags.rs
@@ -4,7 +4,7 @@ use just_fmt::snake_case;
///
/// This enum defines the supported shell types that can be used for
/// generating shell-specific command syntax, scripts, or completions.
-#[derive(Default, Debug, Clone)]
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))]
pub enum ShellFlag {
/// Represents the Bash shell.
diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
index 1fca325..734b5d2 100644
--- a/mingling_core/src/comp/shell_ctx.rs
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -2,7 +2,7 @@
use std::collections::HashSet;
-use crate::{Flag, ShellFlag, Suggest};
+use crate::{Flag, ShellFlag, Suggest, special_argument};
/// Context passed from the shell to the completion system,
/// providing information about the current command line state
@@ -38,44 +38,26 @@ pub struct ShellContext {
impl TryFrom<Vec<String>> for ShellContext {
type Error = String;
- fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
- use std::collections::HashMap;
-
- // Parse arguments into a map for easy lookup
- let mut arg_map = HashMap::new();
- let mut i = 0;
- while i < args.len() {
- if args[i].starts_with('-') {
- let key = args[i].clone();
- if i + 1 < args.len() && !args[i + 1].starts_with('-') {
- arg_map.insert(key, args[i + 1].clone());
- i += 2;
- } else {
- arg_map.insert(key, String::new());
- i += 1;
- }
- } else {
- i += 1;
- }
- }
-
+ fn try_from(mut args: Vec<String>) -> Result<Self, Self::Error> {
// Extract values with defaults
- let command_line = arg_map.get("-f").cloned().unwrap_or_default();
- let cursor_position = arg_map
- .get("-C")
+ let command_line = special_argument!(args, "-f").unwrap_or_default();
+ let cursor_position = special_argument!(args, "-C")
.and_then(|s| s.parse().ok())
.unwrap_or_default();
- let current_word = arg_map.get("-w").cloned().unwrap_or_default();
- let previous_word = arg_map.get("-p").cloned().unwrap_or_default();
- let command_name = arg_map.get("-c").cloned().unwrap_or_default();
- let word_index = arg_map
- .get("-i")
+ let current_word = special_argument!(args, "-w").unwrap_or_default();
+ let previous_word = special_argument!(args, "-p").unwrap_or_default();
+ let command_name = special_argument!(args, "-c").unwrap_or_default();
+ let word_index = special_argument!(args, "-i")
.and_then(|s| s.parse().ok())
.unwrap_or_default();
- let shell_flag = arg_map
- .get("-F")
- .cloned()
- .map_or(ShellFlag::Other("unknown".to_string()), ShellFlag::from);
+ // Distinguish "-F absent" (unknown shell) from "-F present without a value" (empty shell)
+ let has_shell_flag = args.iter().any(|arg| arg == "-F");
+ let shell_flag = if has_shell_flag {
+ special_argument!(args, "-F")
+ .map_or_else(|| ShellFlag::Other(String::new()), ShellFlag::from)
+ } else {
+ ShellFlag::Other("unknown".to_string())
+ };
let all_words = command_line
.split_whitespace()
diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs
index 88fa5fc..804d622 100644
--- a/mingling_core/src/comp/suggest.rs
+++ b/mingling_core/src/comp/suggest.rs
@@ -56,6 +56,108 @@ impl Suggest {
(suggest, _) => suggest,
}
}
+
+ /// Adds multiple simple suggestions (without descriptions) to the `Suggest` set.
+ ///
+ /// Each item produced by the iterator is wrapped in a [`SuggestItem::Simple`]
+ /// variant and inserted into the underlying `BTreeSet`.
+ ///
+ /// # Arguments
+ ///
+ /// * `items` — A collection of suggestion strings to add.
+ pub fn add_suggest(&mut self, items: impl Into<Vec<String>>) {
+ for item in items.into() {
+ self.insert(SuggestItem::Simple(item));
+ }
+ }
+
+ /// Adds multiple suggestions with a shared description to the `Suggest` set.
+ ///
+ /// Each item produced by the iterator is wrapped in a
+ /// [`SuggestItem::WithDescription`] variant using the provided description,
+ /// and inserted into the underlying `BTreeSet`.
+ ///
+ /// # Arguments
+ ///
+ /// * `items` — A collection of suggestion strings to add.
+ /// * `desc` — The description to attach to each suggestion. Must implement
+ /// `Into<String>`.
+ pub fn add_suggest_with_description(
+ &mut self,
+ items: impl Into<Vec<String>>,
+ desc: impl Into<String>,
+ ) {
+ let desc_str = desc.into();
+ for item in items.into() {
+ 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/src/docs/build.md b/mingling_core/src/docs/build.md
new file mode 100644
index 0000000..6f9285a
--- /dev/null
+++ b/mingling_core/src/docs/build.md
@@ -0,0 +1,57 @@
+Provide Mingling's build script module for build-time behavior of specific features in `build.rs`.
+
+To use it, add a dependency on mingling under `[build-dependencies]` in `Cargo.toml`, and enable the relevant features:
+
+## Build-Time Related Features
+
+| Name | Purpose |
+| ---------------- | ------------------------------------------------------------------------------------------------- |
+| `build` | Master switch for build-time features |
+| `build_advanced` | Master switch for build-time features, paired with the `advanced` feature |
+| `build_full` | Master switch for build-time features, paired with the `full` feature |
+| `comp` | Completion script builder; both sides must enable it, generates cross-platform completion scripts |
+| `pathf` | Type path analyzer; both sides must enable it, generates type mapping tables |
+| `dispatch_tree` | Compile-time dispatch tree; when `pathf` is a build-time dependency, |
+| | and `dispatch_tree` (included in `advanced` or `full`) is enabled, both sides should enable it |
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = [
+ "advanced", # Enable `advanced` if using it
+]
+
+[build-dependencies.mingling]
+features = [
+ "build_advanced" # This side should enable `build_advanced`
+]
+```
+
+## `build.rs` Templates
+
+You can use the following template to write `build.rs` to quickly gain the build-time capabilities of `comp` and `pathf`:
+
+```rust,ignore
+// build.rs
+fn main() {
+ build_scripts();
+ build_pathf_mapping();
+}
+
+/// Generate completion scripts
+fn build_scripts() {
+ // `env!("CARGO_PKG_NAME")` equals the crate name, which matches the binary name.
+ // If your binary name differs from the crate name, specify it explicitly.
+ mingling::build::build_comp_scripts(
+ // Your binary name:
+ env!("CARGO_PKG_NAME"),
+ )
+ .unwrap();
+}
+
+fn build_pathf_mapping() {
+ // Build pathf type mapping to ensure that the enabled `pathf` feature
+ // can correctly scan macros in the project
+ mingling::build::analyze_and_build_type_mapping().unwrap();
+}
+```
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index b1bfe08..3db3d31 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -42,21 +42,8 @@ pub mod core_res {
#[cfg(feature = "comp")]
pub(crate) mod comp;
-/// Provides Mingling's build script module, used in `build.rs` to provide build-time behavior for certain features.
-///
-/// To use it, add the following to your `Cargo.toml` under `[build-dependencies]`, and enable the features
-/// that require build-time behavior from the crate:
-///
-/// ```toml
-/// [build-dependencies.mingling]
-/// version = "0.3.0"
-/// features = [
-/// "build", // Enable it
-/// "comp", // If you need completion-related build-time behavior, enable this as well
-/// ]
-/// ```
#[cfg(feature = "build")]
-#[doc(hidden)]
+#[doc = include_str!("docs/build.md")]
pub mod build;
// Public Modules
@@ -83,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::*;
@@ -115,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 14705ac..5b1152a 100644
--- a/mingling_core/src/program/collection.rs
+++ b/mingling_core/src/program/collection.rs
@@ -22,14 +22,14 @@ pub trait ProgramCollect {
/// Enum type representing internal IDs for the program
type Enum;
/// Error type when a dispatcher is not found for the given member
- type ErrorDispatcherNotFound: Grouped<Self::Enum>;
+ type EntryFallback: Grouped<Self::Enum>;
/// Error type when a renderer is not found for the given member
type ErrorRendererNotFound: Grouped<Self::Enum>;
/// Result type for an empty chain result
///
- /// When the `extra_macros` feature is enabled,
+ /// When the `extras` feature is enabled,
/// you can use the `empty_result!()` macro to create this
type ResultEmpty: Grouped<Self::Enum>;
@@ -55,7 +55,7 @@ pub trait ProgramCollect {
fn build_renderer_not_found(member_id: Self::Enum) -> AnyOutput<Self::Enum>;
/// Build an [`AnyOutput`](./struct.AnyOutput.html) to indicate that a dispatcher was not found
- fn build_dispatcher_not_found(args: Vec<String>) -> AnyOutput<Self::Enum>;
+ fn build_entry_fallback(args: Vec<String>) -> AnyOutput<Self::Enum>;
/// Build an [`AnyOutput`](./struct.AnyOutput.html) to indicate that the chain returned an empty result
fn build_empty_result() -> AnyOutput<Self::Enum>;
@@ -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(
diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs
index dbe4789..cd2abf5 100644
--- a/mingling_core/src/program/collection/mock.rs
+++ b/mingling_core/src/program/collection/mock.rs
@@ -34,7 +34,7 @@ unsafe impl Grouped<MockProgramCollect> for MockProgramCollect {
impl ProgramCollect for MockProgramCollect {
type Enum = MockProgramCollect;
- type ErrorDispatcherNotFound = MockProgramCollect;
+ type EntryFallback = MockProgramCollect;
type ErrorRendererNotFound = MockProgramCollect;
type ResultEmpty = MockProgramCollect;
@@ -54,7 +54,7 @@ impl ProgramCollect for MockProgramCollect {
unreachable!()
}
- fn build_dispatcher_not_found(_args: Vec<String>) -> AnyOutput<Self::Enum> {
+ fn build_entry_fallback(_args: Vec<String>) -> AnyOutput<Self::Enum> {
unreachable!()
}
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index f0322a5..d9b4dd8 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -45,7 +45,7 @@ where
}
// Current
- let mut current = C::build_dispatcher_not_found(vec![]);
+ let mut current = C::build_entry_fallback(vec![]);
// Run hooks
control!(
@@ -193,7 +193,7 @@ where
}
Err(ProgramInternalExecuteError::DispatcherNotFound) => {
// No matching Dispatcher is found
- C::build_dispatcher_not_found(args.to_vec())
+ C::build_entry_fallback(args.to_vec())
}
Err(e) => return Err(e),
};
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 7d94a21..50c53d7 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -713,7 +713,7 @@ mod tests {
impl ProgramCollect for MockHookEnum {
type Enum = MockHookEnum;
- type ErrorDispatcherNotFound = MockHookEnum;
+ type EntryFallback = MockHookEnum;
type ErrorRendererNotFound = MockHookEnum;
type ResultEmpty = MockHookEnum;
@@ -721,7 +721,7 @@ mod tests {
unreachable!()
}
- fn build_dispatcher_not_found(_args: Vec<String>) -> crate::AnyOutput<MockHookEnum> {
+ fn build_entry_fallback(_args: Vec<String>) -> crate::AnyOutput<MockHookEnum> {
unreachable!()
}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index fc3f2b1..3e63a00 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -350,7 +350,7 @@ impl RenderResult {
pub fn eprintln(&mut self, text: impl Into<String>) {
let text = text.into();
if self.immediate_output {
- println!("{}", text)
+ eprintln!("{}", text)
}
self.append_line_to_buffer(text, Stderr);
}
diff --git a/mingling_core/tests/test-all/Cargo.toml b/mingling_core/tests/test-all/Cargo.toml
index a63b6e6..272f1d2 100644
--- a/mingling_core/tests/test-all/Cargo.toml
+++ b/mingling_core/tests/test-all/Cargo.toml
@@ -14,7 +14,7 @@ mingling = { path = "../../../mingling", features = [
"repl",
"dispatch_tree",
"parser",
- "extra_macros",
+ "extras",
] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
diff --git a/mingling_core/tests/test-repl/Cargo.toml b/mingling_core/tests/test-repl/Cargo.toml
index c4c83ce..0924943 100644
--- a/mingling_core/tests/test-repl/Cargo.toml
+++ b/mingling_core/tests/test-repl/Cargo.toml
@@ -7,4 +7,4 @@ publish = false
[workspace]
[dependencies]
-mingling = { path = "../../../mingling", features = ["repl", "extra_macros"] }
+mingling = { path = "../../../mingling", features = ["repl", "extras"] }
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/fish.fish b/mingling_core/tmpls/comps/fish.fish
index 8be948a..64b4ed3 100644
--- a/mingling_core/tmpls/comps/fish.fish
+++ b/mingling_core/tmpls/comps/fish.fish
@@ -5,7 +5,6 @@ function __<<<bin_name>>>_fish_complete
set -l cursor (commandline -C)
set -l current_token (commandline -ct)
- # Calculate current word and word index
set -l current_word ""
set -l previous_word ""
set -l word_index 0
@@ -45,7 +44,6 @@ function __<<<bin_name>>>_fish_complete
end
end
- # Handle cursor after last word
if test $word_index -eq 0 -a (count $cmdline) -gt 0
set word_index (count $cmdline)
if test -n "$current_token" -a "$current_token" != "$cmdline[-1]"
@@ -56,17 +54,14 @@ function __<<<bin_name>>>_fish_complete
set previous_word $cmdline[-1]
end
- # Ensure word_index is within bounds
if test $word_index -gt (count $cmdline)
set word_index (count $cmdline)
end
- # Replace hyphens with carets for jvn_comp
set -l buffer_replaced (string replace -a "-" "^" -- "$buffer")
set -l current_word_replaced (string replace -a "-" "^" -- "$current_word")
set -l previous_word_replaced (string replace -a "-" "^" -- "$previous_word")
- # Build args array
set -l args
set -a args -f "$buffer_replaced" -C "$cursor" -w "$current_word_replaced" -p "$previous_word_replaced"
@@ -78,7 +73,6 @@ function __<<<bin_name>>>_fish_complete
set -a args -i "$word_index"
- # Replace hyphens in all words
if test (count $cmdline) -gt 0
set -l all_words_replaced
for word in $cmdline
@@ -103,10 +97,8 @@ function __<<<bin_name>>>_fish_complete
set -a args -a ""
end
- # Add shell type argument
set -a args -F "fish"
- # Call jvn_comp and handle output
set -l output
if not <<<bin_name>>> __comp $args 2>/dev/null | read -z output
return
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")