diff options
Diffstat (limited to 'mingling_core')
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") |
