diff options
43 files changed, 500 insertions, 149 deletions
@@ -12,6 +12,8 @@ docs/cov-test/ # Drafts __*.md __*/ +__*.py +__*.rs # Fuck nul diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs index 4b6f973..39d55eb 100644 --- a/.run/src/bin/ci.rs +++ b/.run/src/bin/ci.rs @@ -1,4 +1,5 @@ use std::io::Write as _; +use std::path::{Path, PathBuf}; use std::process::exit; use arg_picker::{Picker, macros::arg}; @@ -7,11 +8,7 @@ use tools::{ }; fn get_ignore_dirs() -> Vec<String> { - vec![ - ".temp".to_string(), - "mling/res".to_string(), - "mling\\res".to_string(), - ] + vec![".temp".to_string()] } fn print_help() { @@ -122,6 +119,9 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { println_cargo_style!("Phase: Test all crates"); test_all()?; + + println_cargo_style!("Phase: Test arg picker"); + test_arg_picker()?; } if run_all || test_docs { @@ -167,16 +167,56 @@ fn test_docs_code_blocks() -> Result<(), i32> { ) } +/// Returns the manifest paths of all workspace members (via `cargo metadata --no-deps`). +/// +/// These crates are tested/built/clipped together with `--workspace` so that +/// feature-gated code is covered, instead of relying on each crate's default features. +fn workspace_manifests() -> Vec<PathBuf> { + let Ok(output) = tools::run_cmd_capture("cargo metadata --no-deps --format-version 1") else { + return Vec::new(); + }; + let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else { + return Vec::new(); + }; + json["packages"] + .as_array() + .into_iter() + .flatten() + .filter_map(|p| p["manifest_path"].as_str().map(PathBuf::from)) + .collect() +} + +fn same_path(a: &Path, b: &Path) -> bool { + let norm = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()); + norm(a) == norm(b) +} + fn build_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: build with all documented features (same set used by cov-test) + let features_arg = doc_features_arg(); + tasks.push(( + "Build: workspace".to_string(), + "workspace".to_string(), + format!("cargo build --workspace{features_arg} --color always"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } let label = format!("Build: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( @@ -191,13 +231,29 @@ fn build_all() -> Result<(), i32> { fn clippy_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: clippy with all documented features + let features_arg = doc_features_arg(); + tasks.push(( + "Clippy: workspace".to_string(), + "workspace".to_string(), + format!("cargo clippy --workspace{features_arg} --color always -- -D warnings"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } let label = format!("Clippy: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( @@ -209,17 +265,43 @@ fn clippy_all() -> Result<(), i32> { run_parallel("Clippy", tasks) } +/// ` --features "<docs.rs features>"` (empty string when unavailable) +fn doc_features_arg() -> String { + match tools::read_features() { + Ok(features) if !features.is_empty() => format!(" --features \"{}\"", features.join(",")), + _ => String::new(), + } +} + fn test_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: test with all documented features so that feature-gated + // tests (comp/repl/picker/structural_renderer/...) are actually executed. + // `arg-picker` is excluded here and tested separately via [`test_arg_picker`]. + let features_arg = doc_features_arg(); + tasks.push(( + "Test: workspace".to_string(), + "workspace".to_string(), + format!("cargo test --workspace{features_arg} --exclude arg-picker --color always"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } - let label = format!("Testing: {}", cargo_toml.to_string_lossy()); + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } + let label = format!("Test: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( "cargo test --manifest-path {} --color always", @@ -230,6 +312,15 @@ fn test_all() -> Result<(), i32> { run_parallel("Testing", tasks) } +/// `arg-picker` is excluded from the workspace test command: when built with +/// `mingling_support` (enabled via `mingling/picker`), its README doctests +/// expand `arg!` to `::mingling::picker::PickerArg`, which is not available +/// inside the arg-picker crate itself. Test it separately with its default +/// features instead. +fn test_arg_picker() -> Result<(), i32> { + run_cmd!("cargo test -p arg-picker --color always") +} + fn deploy_api_docs() -> Result<(), i32> { run_cmd!( "cargo run --manifest-path .run/Cargo.toml --color always --bin deploy-api-docs -- --docsrs" diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1 index bebe9ff..7fba62e 100644 --- a/.run/src/bin/install-mling.ps1 +++ b/.run/src/bin/install-mling.ps1 @@ -1,4 +1,4 @@ -cargo install --path mling +cargo install --path mingling_cli New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null # Copy all files containing _comp from the debug directory diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh index 5f2ee7a..5b5e7b2 100644..100755 --- a/.run/src/bin/install-mling.sh +++ b/.run/src/bin/install-mling.sh @@ -1,6 +1,6 @@ #!/bin/bash -cargo install --path mling +cargo install --path mingling_cli mkdir -p .temp/comp cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found" diff --git a/CHANGELOG.md b/CHANGELOG.md index baa7f19..aa80b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,25 @@ None #### Fixes: -None +1. **[`comps:zsh`]** Fixed zsh completion script to properly escape colons in completion descriptions. The zsh completion script generated for the `zsh` output format now escapes colon characters in completion items (`${item//:/\\:}`) and description parts (`${match[1]//:/\\:}`) so that descriptions containing colons don't break the `_describe` command's parsing. Additionally, fixed the simple-completions branch to iterate over the original `completions` array (with the colon-escaped format matching) rather than the already-parsed `parsed_completions` array, correctly extracting the completion item when no description is present. + +2. **[`comps:bash`]** Reworked the bash completion script template to fix word-index tracking when the cursor is in the middle of a word and to add colon-ltrimming for completion descriptions. The script now computes the current word (`cur`) using `COMP_LINE` truncated to `COMP_POINT` (taking the last whitespace-delimited token) rather than relying on `COMP_WORDS[COMP_CWORD]`, which fails when the cursor is in the middle of a word. The word index is derived from the count of words preceding the current cursor position (`before_words`), and `prev` is the last word in that preceding set. The option flags to the underlying completion engine are now passed as `-f value`/`-C value`/etc. (space-separated) instead of `-f=value`/`-C=value`/etc. (equals-separated), since the engine expects space-delimited argument pairs. Additionally, when the current word contains a colon and `COMP_WORDBREAKS` includes `:`, the completion items are trimmed of the colon prefix before being inserted into `COMPREPLY` — this prevents completions like `foo:bar` from being double-prefixed with the literal `foo:bar` when bash would otherwise append the full word. + +3. **[`core:comp`]** Fixed the completion engine to correctly resolve command nodes when global parameters (flags and their values) precede the subcommand. Previously, the engine matched the command tree starting from the first argument after the program name, treating every leading argument as part of the command path — so `prog [PARAM]... <subcommand>` style invocations would fail to match any node. Now: + + - A new helper `first_command_arg_index::<P>(args)` scans the argument list and returns the index of the first argument that matches the head of a registered command node (excluding node names starting with `_`). Everything before that index is treated as global parameters and skipped during command tree matching. + - In `CompletionHelper::complete`, the dispatch args are sliced to start at the first command-node match (`all_args[start..]`); if no node match is found, the args are empty (`Vec::new()`). + - In `default_completion`, the input path resolution skips the leading global-parameter arguments the same way, so `prog -v hello` correctly suggests the `hello` node even though `-v` precedes it. + - In the unmatched-dispatcher branch: when a command node _has_ been matched (`first_cmd_match.is_some()`), the `EntryFallback` handler is **skipped** and only `default_completion` runs, since global parameters do not warrant invoking the fallback. When no node was matched, the previous behavior is retained (fallback combined with default completion). + + This enables `prog [PARAM]... <subcommand>` style invocations to resolve the subcommand correctly, while a "broken" path such as `prog -v hello -a someone` still fails to match a `hello someone` node (since `-a someone` lies _after_ the first matched node and participates normally). + +4. **[`core:comp`]** Added `add_prefix()` and `add_suffix()` methods to `Suggest` for batch-transforming suggestion text: + + - **`add_prefix(self, prefix: impl Into<String>) -> Suggest`** — Takes the current `Suggest` value and prepends the given prefix to the suggestion text of every item. If the `Suggest` value is `Suggest::FileCompletion`, it is returned unchanged. For example, `["foo", "bar"]` with prefix `"--"` becomes `["--foo", "--bar"]`. + - **`add_suffix(self, suffix: impl Into<String>) -> Suggest`** — Takes the current `Suggest` value and appends the given suffix to the suggestion text of every item. If the `Suggest` value is `Suggest::FileCompletion`, it is returned unchanged. For example, `["foo", "bar"]` with suffix `"="` becomes `["foo=", "bar="]`. + + Both methods consume the original `Suggest` value and return a new one, enabling ergonomic chaining with the existing `combine()` method for transforming completion suggestion sets. #### Optimizations: @@ -168,6 +186,29 @@ None Also reorganized the `[features]` section of `mingling/Cargo.toml` into logical subsections (Presets, Core, Special features, Features, LEGACY) for improved maintainability and documentation. +8. **[`core`]** **[`macros`]** Added the `#[completion(EntryFallback)]` syntax to the `#[completion]` macro, mirroring the `#[help(EntryFallback)]` pattern. When the `EntryFallback` identifier is passed (either as a bare path in the attribute arguments), the macro generates a chain handler for the `EntryFallback` type (the program's fallback entry pack type generated by `gen_program!()`). This handler is invoked during the default-completion path when no explicit dispatcher match is found. + + When the program's dispatcher fails to find a match in the completion pipeline, the `CompletionHelper::complete` method now: + + - Checks for an explicit completion handler via the chain pipeline for the `EntryFallback` type (calling `P::do_comp(&P::build_entry_fallback(vec![]), ctx)`). + - If that produces suggestions, they are combined with the default completion suggestions via `Suggest::combine()` (which merges `BTreeSet`s for `Suggest::Suggest` values). + - Falls back to the standard `default_completion::<P>(ctx)` behavior when no explicit fallback handler yields results. + + This enables users to provide custom completion suggestions for the fallback/unmatched case: + + ```rust,ignore + #[completion(EntryFallback)] + fn complete_fallback(_ctx: &ShellContext) -> Suggest { + suggest! { "fallback" } + } + ``` + + The generated chain handler for `EntryFallback` is identical to how `#[chain]`-annotated functions targeting entry pack types work — the `EntryFallback` type name is resolved through the same `chain` code-generation path used for `Entry{Type}` types, allowing users to write a dedicated completion function for the fallback entry point without manually registering it in the program's dispatcher. + + Internal changes: + - `mingling_macros/src/attr/completion.rs` updated to detect the `EntryFallback` identifier in attribute arguments. + - `CompletionHelper::complete` in `mingling_core/src/comp.rs` now invokes the fallback completion handler via `P::do_comp(&P::build_entry_fallback(vec![]), ctx)` and merges results with `Suggest::combine()`. + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`. @@ -192,6 +233,34 @@ None _No behavioral changes — this is a pure feature rename. The `extras` feature provides identical functionality to the old `extra_macros` feature; all prelude and macros module re-exports remain the same under the new feature name._ +2. **[`core`]** **[BREAKING RENAME]** Renamed the internal fallback type `ErrorDispatcherNotFound` and its associated `ProgramCollect` plumbing to `EntryFallback` / `build_entry_fallback`. + + **Associated type renames (on `ProgramCollect`):** + + - `type ErrorDispatcherNotFound` → `type EntryFallback` + - `fn build_dispatcher_not_found(args)` → `fn build_entry_fallback(args)` + + **Type/enum renames generated by `gen_program!()`:** + + - Enum variant `ThisProgram::ErrorDispatcherNotFound` → `ThisProgram::EntryFallback` + - Pack type `ErrorDispatcherNotFound` → `EntryFallback` (created by `program_fallback_gen!()` via `pack!(EntryFallback = Vec<String>)`) + + **Migration guide (for downstream code):** + + - Renderer/help functions that previously took `ErrorDispatcherNotFound` as a parameter must now take `EntryFallback`. + - Any code referencing `C::build_dispatcher_not_found(...)` must now call `C::build_entry_fallback(...)`. + - Any manual `ProgramCollect` implementation (e.g., in tests or mocks) must rename both the associated type `ErrorDispatcherNotFound` → `EntryFallback` and the associated method `build_dispatcher_not_found` → `build_entry_fallback`. + + _No behavioral changes — this is a pure rename of the internal fallback type and its associated `ProgramCollect` methods. The type's semantics, shape (wrapping `Vec<String>`), and rendering behavior are unchanged. + +3. **[`picker:global`]** **[`setups:picker`]** Refactored the global picker utility functions into a trait-based API. The standalone functions `pick_global_flag(program, flag)` and `pick_global_argument(program, arg)` in `mingling::picker` have been replaced by the `PickerHelper<C>` trait, implemented for `Program<C>`. This trait provides `pick_flag(&mut self, flag: &PickerArg<Flag>) -> bool` and `pick_argument<A>(&mut self, arg: &PickerArg<A>) -> Option<A>`, and is backed by the `take_args` / `replace_args` methods on `Program`. + + **Migration guide:** + + - `pick_global_flag(program, flag)` → `program.pick_flag(flag)` + - `pick_global_argument(program, arg)` → `program.pick_argument(arg)` + - Import `mingling::picker::PickerHelper` instead of `mingling::picker::{pick_global_flag, pick_global_argument}` + --- ### Release 0.3.0 (2026-07-27) diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md index 7d451e0..add49eb 100644 --- a/GETTING-STARTED.md +++ b/GETTING-STARTED.md @@ -117,7 +117,7 @@ use mingling::prelude::*; use std::io::Write; #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut result = RenderResult::new(); writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); result @@ -313,7 +313,7 @@ fn render_too_long(len: ErrorNameTooLong) -> RenderResult { Two built-in fallback types are always available: -- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input +- `EntryFallback` — rendered when no dispatcher matches the input - `ErrorRendererNotFound` — rendered when no renderer is found for a result type --- diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs index 0b5176d..32824ea 100644 --- a/arg_picker/src/arg.rs +++ b/arg_picker/src/arg.rs @@ -1,4 +1,4 @@ -use crate::{Pickable, PickerArgInfo, SinglePickable, parselib::ParserStyle}; +use crate::{Pickable, PickerArgInfo, parselib::ParserStyle}; use std::marker::PhantomData; /// Represents a constraint definition for a parameter selection. @@ -166,7 +166,7 @@ where impl<'a, Type> From<PickerArg<'a, Type>> for Vec<String> where - Type: SinglePickable, + Type: Pickable<'a>, { fn from(value: PickerArg<'a, Type>) -> Self { let mut result = Vec::new(); diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md index 1c4e410..a399ae3 100644 --- a/docs/_zh_CN/pages/10-help.md +++ b/docs/_zh_CN/pages/10-help.md @@ -27,14 +27,14 @@ fn help_greet(_entry: EntryGreet) { ## 全局帮助 -你也可以为 `ErrorDispatcherNotFound` 写帮助,作为"根帮助": +你也可以为 `EntryFallback` 写帮助,作为"根帮助": ```rust @@@use mingling::macros::help; @@@use mingling::macros::buffer; // 用户直接输入 --help 时触发 #[help(buffer)] -fn help_root(entry: ErrorDispatcherNotFound) { +fn help_root(entry: EntryFallback) { r_println!("Usage: my-cli <command>"); r_println!("Commands:"); r_println!(" greet Say hello"); @@ -42,7 +42,7 @@ fn help_root(entry: ErrorDispatcherNotFound) { ``` > [!TIP] -> `ErrorDispatcherNotFound` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。 +> `EntryFallback` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。 ## 需要 Setup 配合 diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md index 8b29fca..7f66c71 100644 --- a/docs/_zh_CN/pages/4-render-result.md +++ b/docs/_zh_CN/pages/4-render-result.md @@ -116,13 +116,13 @@ cargo run -- great ## 补上 Fallback -`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer: +`gen_program!()` 自动生成了一个 `EntryFallback` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer: ```rust use mingling::macros::buffer; #[renderer(buffer)] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { +fn render_entry_fallback(err: EntryFallback) { if err.inner.is_empty() { r_println!("Unknown command"); } else { diff --git a/docs/_zh_CN/pages/concepts/1-the-pipeline.md b/docs/_zh_CN/pages/concepts/1-the-pipeline.md index b0bb19d..9c74087 100644 --- a/docs/_zh_CN/pages/concepts/1-the-pipeline.md +++ b/docs/_zh_CN/pages/concepts/1-the-pipeline.md @@ -42,12 +42,12 @@ graph TD graph LR Input["用户输入"] --> M{匹配 Dispatcher} M -->|"匹配到"| E["调用 dispatcher.begin(args)<br/>返回包装好的 Entry"] - M -->|"未匹配"| NF["build_dispatcher_not_found<br/>生成 ErrorDispatcherNotFound"] + M -->|"未匹配"| NF["build_entry_fallback<br/>生成 EntryFallback"] ``` 匹配成功后调用 `dispatcher.begin(args)`,返回 `ChainProcess::Ok((AnyOutput, _))`,即包装好用户输入参数的 Entry 类型。 -如果没有匹配到任何 Dispatcher,则生成 `ErrorDispatcherNotFound`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。 +如果没有匹配到任何 Dispatcher,则生成 `EntryFallback`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。 ### 2. Help 短路 diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md index f5e6b8f..a0236f6 100644 --- a/docs/_zh_CN/pages/concepts/4-program-collect.md +++ b/docs/_zh_CN/pages/concepts/4-program-collect.md @@ -21,7 +21,7 @@ - **`render`** —— 根据 `member_id` 调用对应的 `#[renderer]` 函数,写入 `RenderResult` - **`render_help`** —— 根据 `member_id` 调用对应的 `#[help]` 函数 - **`has_chain` / `has_renderer`** —— 判断某个变体有没有对应的处理函数 -- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况 +- **`build_entry_fallback` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况 这套映射在运行时通过枚举匹配来完成——编译期只生成了枚举和匹配分支,实际的函数调用发生在运行时。 diff --git a/docs/dev/pages/issues/the-shit-time.md b/docs/dev/pages/issues/the-shit-time.md index 9d6c429..ece670e 100644 --- a/docs/dev/pages/issues/the-shit-time.md +++ b/docs/dev/pages/issues/the-shit-time.md @@ -9,7 +9,7 @@ Of course, you can also contribute to this document. --- -## Why is there no fallback completion logic? +## Why is there no fallback completion logic? (Solved) (completion) (fallback) @@ -35,6 +35,17 @@ fn complete(ctx: &ShellContext) -> Suggest { } ``` +Final implementation: + +By adding support for `EntryFallback` (formerly `ErrorDispatcherNotFound`) to the Completion system, `EntryFallback` can now be used as a completion entry point when no subcommand is matched: + +```rust +#[completion(EntryFallback)] +fn complete_fallback(_ctx: &ShellContext) -> Suggest { + suggest! { "fallback" } +} +``` + --- ## Why can't I register descriptions for commands? diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md index 1e3ea78..d9cc557 100644 --- a/docs/pages/10-help.md +++ b/docs/pages/10-help.md @@ -27,14 +27,14 @@ fn help_greet(_entry: EntryGreet) { ## Global Help -You can also write help for `ErrorDispatcherNotFound` as the "root help": +You can also write help for `EntryFallback` as the "root help": ```rust @@@use mingling::macros::help; @@@use mingling::macros::buffer; // Triggered when user passes --help directly #[help(buffer)] -fn help_root(entry: ErrorDispatcherNotFound) { +fn help_root(entry: EntryFallback) { r_println!("Usage: my-cli <command>"); r_println!("Commands:"); r_println!(" greet Say hello"); @@ -42,7 +42,7 @@ fn help_root(entry: ErrorDispatcherNotFound) { ``` > [!TIP] -> `ErrorDispatcherNotFound` is a type generated by `gen_program!()`, representing "no matching command found." Writing `#[help]` for it adds help to the program's root command. +> `EntryFallback` is a type generated by `gen_program!()`, representing "no matching command found." Writing `#[help]` for it adds help to the program's root command. ## Requires Setup diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md index 9e72d09..7b63b45 100644 --- a/docs/pages/4-render-result.md +++ b/docs/pages/4-render-result.md @@ -116,13 +116,13 @@ cargo run -- great ## Adding a Fallback -`gen_program!()` auto-generates an `ErrorDispatcherNotFound` type wrapping `Vec<String>`—it holds the user input that didn't match any command. You just need to write a Renderer for it: +`gen_program!()` auto-generates an `EntryFallback` type wrapping `Vec<String>`—it holds the user input that didn't match any command. You just need to write a Renderer for it: ```rust use mingling::macros::buffer; #[renderer(buffer)] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { +fn render_entry_fallback(err: EntryFallback) { if err.inner.is_empty() { r_println!("Unknown command"); } else { diff --git a/docs/pages/concepts/1-the-pipeline.md b/docs/pages/concepts/1-the-pipeline.md index e73379d..2ee26d0 100644 --- a/docs/pages/concepts/1-the-pipeline.md +++ b/docs/pages/concepts/1-the-pipeline.md @@ -42,12 +42,12 @@ The matching rule is **prefix matching** on space-separated tokens — the longe graph LR Input["user input"] --> M{"match Dispatcher"} M -->|"matched"| E["call dispatcher.begin(args)<br/>return wrapped Entry"] - M -->|"no match"| NF["build_dispatcher_not_found<br/>generate ErrorDispatcherNotFound"] + M -->|"no match"| NF["build_entry_fallback<br/>generate EntryFallback"] ``` On a match, `dispatcher.begin(args)` is called, returning `ChainProcess::Ok((AnyOutput, _))` — the Entry type wrapping the user's input params. -If no Dispatcher matches, `ErrorDispatcherNotFound` is generated (wrapping the full input), which a Renderer can later handle to display "Command not found". +If no Dispatcher matches, `EntryFallback` is generated (wrapping the full input), which a Renderer can later handle to display "Command not found". ### 2. Help Shortcut diff --git a/docs/pages/concepts/4-program-collect.md b/docs/pages/concepts/4-program-collect.md index a24f115..c5203c3 100644 --- a/docs/pages/concepts/4-program-collect.md +++ b/docs/pages/concepts/4-program-collect.md @@ -21,7 +21,7 @@ This enum is the type of `G` in `AnyOutput<G>` — the scheduler uses enum varia - **`render`** — calls the corresponding `#[renderer]` function by `member_id`, writes to `RenderResult` - **`render_help`** — calls the corresponding `#[help]` function by `member_id` - **`has_chain` / `has_renderer`** — checks whether a variant has a corresponding handler -- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** — three built-in fallback types for edge cases +- **`build_entry_fallback` / `build_renderer_not_found` / `build_empty_result`** — three built-in fallback types for edge cases This mapping is resolved at runtime via enum matching — only the enum and match branches are generated at compile time; actual function calls happen at runtime. diff --git a/examples/example-error-handling/src/main.rs b/examples/example-error-handling/src/main.rs index de9792d..0cd973a 100644 --- a/examples/example-error-handling/src/main.rs +++ b/examples/example-error-handling/src/main.rs @@ -94,7 +94,7 @@ fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { /// Renders the error when the dispatcher (subcommand) is not found. #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); writeln!( render_result, diff --git a/examples/example-repl-basic/src/main.rs b/examples/example-repl-basic/src/main.rs index cfd00d1..361488d 100644 --- a/examples/example-repl-basic/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -178,7 +178,7 @@ fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) -> RenderResult /// Handle dispatcher not found event /// Renders the error when a command is not found. #[renderer] -fn dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { +fn dispatcher_not_found(prev: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); writeln!(render_result, "Command not found: \"{}\"", prev.join(", ")).ok(); render_result diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs index e9169df..33ddde0 100644 --- a/examples/example-unit-test/src/main.rs +++ b/examples/example-unit-test/src/main.rs @@ -126,7 +126,7 @@ fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { /// Renders the error when the dispatcher (subcommand) is not found. #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); writeln!( render_result, diff --git a/examples/full-todolist/src/help.rs b/examples/full-todolist/src/help.rs index 2f8228a..48b93f2 100644 --- a/examples/full-todolist/src/help.rs +++ b/examples/full-todolist/src/help.rs @@ -1,12 +1,12 @@ //! This module provides help information for the `todolist` command line program -use crate::{EntryAdd, EntryClean, EntryComplete, EntryList, ErrorDispatcherNotFound}; +use crate::{EntryAdd, EntryClean, EntryComplete, EntryList, EntryFallback}; use mingling::{RenderResult, macros::help}; use std::io::Write; /// Shows the global help message. #[help] -pub fn help_global(_p: ErrorDispatcherNotFound) -> RenderResult { +pub fn help_global(_p: EntryFallback) -> RenderResult { let mut render_result = RenderResult::new(); writeln!( render_result, diff --git a/mingling/src/docs/gen_program.md b/mingling/src/docs/gen_program.md index 91e7b91..16402c0 100644 --- a/mingling/src/docs/gen_program.md +++ b/mingling/src/docs/gen_program.md @@ -7,10 +7,12 @@ You can access them like this: ```rust # pub struct ThisProgram; # impl ThisProgram { fn new() -> Self { ThisProgram } } +# fn main() { // main.rs / lib.rs // Use them here via crate::* let mut program = crate::ThisProgram::new(); +# } // `ThisProgram` is generated here // | diff --git a/mingling/src/docs/lib.md b/mingling/src/docs/lib.md index a7a583b..697f6c5 100644 --- a/mingling/src/docs/lib.md +++ b/mingling/src/docs/lib.md @@ -51,7 +51,7 @@ fn render_name(name: ResultName) -> RenderResult { } #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +fn render_entry_fallback(err: EntryFallback) -> RenderResult { let mut result = RenderResult::default(); if err.len() > 0 { result.println(&format!("Command not found: [{}]", err.join(" "))); diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 6f37a13..cb6bbe7 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1500,7 +1500,7 @@ pub mod example_enum_tag {} /// /// /// Renders the error when the dispatcher (subcommand) is not found. /// #[renderer] -/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +/// fn render_entry_fallback(err: EntryFallback) -> RenderResult { /// let mut render_result = RenderResult::new(); /// writeln!( /// render_result, @@ -2580,7 +2580,7 @@ pub mod example_pathfinder {} /// /// Handle dispatcher not found event /// /// Renders the error when a command is not found. /// #[renderer] -/// fn dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { +/// fn dispatcher_not_found(prev: EntryFallback) -> RenderResult { /// let mut render_result = RenderResult::new(); /// writeln!(render_result, "Command not found: \"{}\"", prev.join(", ")).ok(); /// render_result @@ -2970,7 +2970,7 @@ pub mod example_structural_renderer {} /// /// /// Renders the error when the dispatcher (subcommand) is not found. /// #[renderer] -/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +/// fn render_entry_fallback(err: EntryFallback) -> RenderResult { /// let mut render_result = RenderResult::new(); /// writeln!( /// render_result, diff --git a/mingling/src/gen_program.rs b/mingling/src/gen_program.rs index 18d9117..a3b7e29 100644 --- a/mingling/src/gen_program.rs +++ b/mingling/src/gen_program.rs @@ -23,10 +23,12 @@ pub struct Entry { /// /// It contains the IDs of all types for this program, registered by the `register_type!` macro. pub enum ThisProgram { + /// The generic program entry point. + Entry, /// Indicates that no matching renderer was found for the given output. ErrorRendererNotFound, /// Indicates that no matching dispatcher was found for the given arguments. - ErrorDispatcherNotFound, + EntryFallback, /// Indicates that the result is empty. ResultEmpty, /// Indicates the completion suggestions computed by the program for rendering. @@ -50,7 +52,7 @@ pub struct ErrorRendererNotFound { /// /// This type is created by the `pack!` macro as a variant of the /// program's output type set (`ThisProgram`). -pub struct ErrorDispatcherNotFound { +pub struct EntryFallback { /// The arguments provided by the user pub(crate) inner: Vec<String>, } @@ -137,9 +139,9 @@ unsafe impl Grouped<ThisProgram> for ErrorRendererNotFound { // However, these are marked `unsafe` because the `Grouped` trait requires the // implementor to guarantee that the type is the only one associated with the // given enum variant — a guarantee that should be carefully verified in production code. -unsafe impl Grouped<ThisProgram> for ErrorDispatcherNotFound { +unsafe impl Grouped<ThisProgram> for EntryFallback { fn member_id() -> ThisProgram { - ThisProgram::ErrorDispatcherNotFound + ThisProgram::EntryFallback } } @@ -184,7 +186,7 @@ unsafe impl Grouped<ThisProgram> for CompletionSuggest { impl ProgramCollect for ThisProgram { type Enum = ThisProgram; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type EntryFallback = EntryFallback; type ErrorRendererNotFound = ErrorRendererNotFound; @@ -194,7 +196,7 @@ impl ProgramCollect for ThisProgram { todo!() } - fn build_dispatcher_not_found(_args: Vec<String>) -> mingling_core::AnyOutput<Self::Enum> { + fn build_entry_fallback(_args: Vec<String>) -> mingling_core::AnyOutput<Self::Enum> { todo!() } diff --git a/mingling/src/picker/global.rs b/mingling/src/picker/global.rs index c203524..f062671 100644 --- a/mingling/src/picker/global.rs +++ b/mingling/src/picker/global.rs @@ -3,33 +3,65 @@ use mingling_core::{Program, ProgramCollect}; use crate::consts::REMAINS; -/// Picks a global flag from the program's arguments. +/// Provides helper methods for picking arguments from a [`Program`]'s argument list. /// -/// This function takes ownership of the program's current arguments, picks the specified `flag` -/// from them, and then returns the remaining arguments back to the program. It returns the -/// boolean value of the flag. -pub fn pick_global_flag<C>(program: &mut Program<C>, flag: &PickerArg<Flag>) -> bool +/// This trait abstracts the functionality of extracting specific arguments or flags +/// from the program's current arguments, while restoring any remaining arguments +/// back into the program. +pub trait PickerHelper<C> where C: ProgramCollect<Enum = C>, { - let args = program.take_args(); - let (flag, args) = args.pick(flag).pick(&REMAINS).unwrap(); - program.replace_args(args.into()); - *flag + /// Takes ownership of the program's current arguments. + /// + /// Returns the program's argument list as a [`Vec<String>`], leaving the program + /// with no arguments until [`replace_args`] is called. + /// + /// [`replace_args`]: PickerHelper::replace_args + fn take_args(&mut self) -> Vec<String>; + + /// Replaces the program's current arguments with the provided list. + /// + /// Returns the previous argument list that was replaced. + fn replace_args(&mut self, args: Vec<String>) -> Vec<String>; + + /// Picks a flag from the program's arguments. + /// + /// This function takes ownership of the program's current arguments, picks the specified `flag` + /// from them, and then returns the remaining arguments back to the program. It returns the + /// boolean value of the flag. + fn pick_flag(&mut self, flag: &PickerArg<Flag>) -> bool { + let args = self.take_args(); + let (flag, args) = args.pick(flag).pick(&REMAINS).unwrap(); + self.replace_args(args.into()); + *flag + } + + /// Picks a argument from the program's arguments. + /// + /// This function takes ownership of the program's current arguments, picks the specified `arg` + /// from them, and then returns the remaining arguments back to the program. It returns the + /// picked argument value, or `None` if the argument was not present. + fn pick_argument<A>(&mut self, arg: &PickerArg<A>) -> Option<A> + where + A: for<'a> Pickable<'a> + Default, + { + let args = self.take_args(); + let (arg, remains) = args.pick(arg).pick(&REMAINS).unpack(); + self.replace_args(remains.unwrap().into()); + arg + } } -/// Picks a global argument from the program's arguments. -/// -/// This function takes ownership of the program's current arguments, picks the specified `arg` -/// from them, and then returns the remaining arguments back to the program. It returns the -/// picked argument value, or `None` if the argument was not present. -pub fn pick_global_argument<C, A>(program: &mut Program<C>, arg: &PickerArg<A>) -> Option<A> +impl<C> PickerHelper<C> for Program<C> where - A: for<'a> Pickable<'a> + Default, C: ProgramCollect<Enum = C>, { - let args = program.take_args(); - let (arg, remains) = args.pick(arg).pick(&REMAINS).unpack(); - program.replace_args(remains.unwrap().into()); - arg + fn take_args(&mut self) -> Vec<String> { + self.take_args() + } + + fn replace_args(&mut self, args: Vec<String>) -> Vec<String> { + self.replace_args(args) + } } diff --git a/mingling/src/setups/picker/basic.rs b/mingling/src/setups/picker/basic.rs index c9f82b3..52bda3d 100644 --- a/mingling/src/setups/picker/basic.rs +++ b/mingling/src/setups/picker/basic.rs @@ -3,7 +3,7 @@ use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; use crate::{ consts::{CONFIRM_FLAG, HELP_FLAG, QUIET_FLAG}, - picker::pick_global_flag, + picker::PickerHelper, }; /// Performs basic program initialization: @@ -43,7 +43,7 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - let help = pick_global_flag(program, self.flag); + let help = program.pick_flag(self.flag); if help { program.user_context.help = true; } @@ -75,7 +75,7 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - let help = pick_global_flag(program, self.flag); + let help = program.pick_flag(self.flag); if help { program.stdout_setting.quiet = true; } @@ -107,7 +107,7 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - let help = pick_global_flag(program, self.flag); + let help = program.pick_flag(self.flag); if help { program.user_context.confirm = true; } diff --git a/mingling/src/setups/picker/structural_renderer.rs b/mingling/src/setups/picker/structural_renderer.rs index 1fa48fd..91848e4 100644 --- a/mingling/src/setups/picker/structural_renderer.rs +++ b/mingling/src/setups/picker/structural_renderer.rs @@ -1,9 +1,6 @@ use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; -use crate::{ - consts::RENDERER_ARG, - picker::{pick_global_argument, pick_global_flag}, -}; +use crate::{consts::RENDERER_ARG, picker::PickerHelper}; /// Sets up the structural renderer for the program: /// @@ -15,7 +12,7 @@ where C: ProgramCollect<Enum = C>, { fn setup(self, program: &mut Program<C>) { - if let Some(renderer) = pick_global_argument(program, &RENDERER_ARG) { + if let Some(renderer) = program.pick_argument(&RENDERER_ARG) { program.structural_renderer_name = renderer.into(); } } @@ -49,27 +46,27 @@ where { fn setup(self, program: &mut Program<C>) { #[cfg(feature = "json_serde_fmt")] - if pick_global_flag(program, &crate::consts::JSON_FLAG) { + if program.pick_flag(&crate::consts::JSON_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::Json; } #[cfg(feature = "json_serde_fmt")] - if pick_global_flag(program, &crate::consts::JSON_PRETTY_FLAG) { + if program.pick_flag(&crate::consts::JSON_PRETTY_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::JsonPretty; } #[cfg(feature = "yaml_serde_fmt")] - if pick_global_flag(program, &crate::consts::YAML_FLAG) { + if program.pick_flag(&crate::consts::YAML_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::Yaml; } #[cfg(feature = "toml_serde_fmt")] - if pick_global_flag(program, &crate::consts::TOML_FLAG) { + if program.pick_flag(&crate::consts::TOML_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::Toml; } #[cfg(feature = "ron_serde_fmt")] - if pick_global_flag(program, &crate::consts::RON_FLAG) { + if program.pick_flag(&crate::consts::RON_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::Ron; } #[cfg(feature = "ron_serde_fmt")] - if pick_global_flag(program, &crate::consts::RON_PRETTY_FLAG) { + if program.pick_flag(&crate::consts::RON_PRETTY_FLAG) { program.structural_renderer_name = crate::StructuralRendererSetting::RonPretty; } } diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs index 0472056..b594b9d 100644 --- a/mingling_cli/src/linter/mlint_report.rs +++ b/mingling_cli/src/linter/mlint_report.rs @@ -10,7 +10,7 @@ use cargo_metadata::{Message, PackageId}; use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING}; use annotate_snippets::{AnnotationKind, Group, Patch, Renderer, Snippet}; use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer}; -use mingling::{AnyOutput, ProgramCollect, Routable}; +use mingling::{RendererInvoker, Routable}; use crate::Next; use crate::metadata::setup::ResUsingJson; @@ -437,9 +437,13 @@ pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) { } #[renderer(buffer)] -pub fn render_lint_reports_json(reports: ResultLintReportsJson) { +pub fn render_lint_reports_json( + reports: ResultLintReportsJson, + message_renderer: &RendererInvoker<Message>, +) { for report in reports.inner { - // DIRTY: Dispatch to the Message renderer using AnyOutput to obtain the render result and append it to the Buffer - r_append!(|| { crate::ThisProgram::render(AnyOutput::new(report.to_compiler_message())) }); + let message = report.to_compiler_message(); + let result = message_renderer.invoke(message); + r_append!(result); } } diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs index d8dcfbd..5f851ca 100644 --- a/mingling_core/src/comp.rs +++ b/mingling_core/src/comp.rs @@ -107,7 +107,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 +148,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 { @@ -163,8 +172,22 @@ impl CompletionHelper { suggest } 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 +235,30 @@ 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())) + }) +} + fn default_completion<P>(ctx: &ShellContext) -> Suggest where P: ProgramCollect<Enum = P> + Display + 'static, @@ -238,14 +285,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 diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs index 8efbc9d..734b5d2 100644 --- a/mingling_core/src/comp/shell_ctx.rs +++ b/mingling_core/src/comp/shell_ctx.rs @@ -50,8 +50,14 @@ impl TryFrom<Vec<String>> for ShellContext { let word_index = special_argument!(args, "-i") .and_then(|s| s.parse().ok()) .unwrap_or_default(); - let shell_flag = special_argument!(args, "-F") - .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 dda5026..804d622 100644 --- a/mingling_core/src/comp/suggest.rs +++ b/mingling_core/src/comp/suggest.rs @@ -92,6 +92,72 @@ impl Suggest { 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/program/collection.rs b/mingling_core/src/program/collection.rs index fa062cf..1b4d7dd 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -22,7 +22,7 @@ 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>; @@ -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>; 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/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/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") diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs index 35c5c30..9598fa8 100644 --- a/mingling_macros/src/attr/command.rs +++ b/mingling_macros/src/attr/command.rs @@ -179,7 +179,7 @@ fn build_ext_attrs(exts: &[syn::Path]) -> Vec<TokenStream2> { /// Returns `true` if the function has a first non-reference (owned) parameter that /// serves as the "args" input. fn has_args_param(sig: &syn::Signature) -> bool { - sig.inputs.first().map_or(false, |arg| { + sig.inputs.first().is_some_and(|arg| { if let FnArg::Typed(pat_type) = arg { !matches!(&*pat_type.ty, Type::Reference(_)) } else { @@ -202,10 +202,10 @@ fn build_wrapper_params( // First param is owned (args) -> replace its type with entry type let mut params = sig.inputs.clone(); if let Some(FnArg::Typed(first)) = params.first_mut() { - first.ty = Box::new(Type::Path(syn::TypePath { + *first.ty = Type::Path(syn::TypePath { qself: None, path: syn::Path::from(entry_type.clone()), - })); + }); } params } else { diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 40f7d47..218750c 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -39,7 +39,7 @@ impl Parse for ClapOptions { error_struct = Some(value); } else if key == "help" { let value: LitBool = input.parse()?; - if value.value() == false { + if !value.value() { // help = false is allowed but does nothing help_enabled = false; } else { @@ -171,7 +171,7 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke }; let dispatch_tree_entry = - get_dispatch_tree_entry(&command_name_str, dispatcher_struct, &struct_name); + get_dispatch_tree_entry(&command_name_str, dispatcher_struct, struct_name); let expanded = quote! { // Keep the original struct definition diff --git a/mingling_macros/src/func/program_fallback_gen.rs b/mingling_macros/src/func/program_fallback_gen.rs index 3d095e5..93d8616 100644 --- a/mingling_macros/src/func/program_fallback_gen.rs +++ b/mingling_macros/src/func/program_fallback_gen.rs @@ -16,7 +16,7 @@ pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { let expanded = quote! { ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); + ::mingling::macros::pack!(EntryFallback = Vec<String>); #pack_empty }; TokenStream::from(expanded) diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs index 0eed1db..e8545f4 100644 --- a/mingling_macros/src/func/program_final_gen.rs +++ b/mingling_macros/src/func/program_final_gen.rs @@ -298,15 +298,15 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { impl ::mingling::ProgramCollect for #name { type Enum = #name; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type EntryFallback = EntryFallback; type ErrorRendererNotFound = ErrorRendererNotFound; type ResultEmpty = ResultEmpty; fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) } - fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + fn build_entry_fallback(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(EntryFallback::new(args)) } fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { ::mingling::AnyOutput::new(ResultEmpty) diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index b6656da..c955e36 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -36,7 +36,7 @@ //! │ V │ //! │ Reads all registries → generates ThisProgram with: │ //! │ • ProgramCollect impl (dispatch/render/chain dispatch tree) │ -//! │ • Fallback types (ErrorDispatcherNotFound, etc.) │ +//! │ • Fallback types (EntryFallback, etc.) │ //! │ • Completion logic (if `comp` feature enabled) │ //! └──────────────────────────────────────────────────────────────────┘ //! ``` @@ -120,8 +120,8 @@ //! ```rust,ignore //! // Example of what gen_program! generates (simplified): //! impl ProgramCollect for ThisProgram { -//! fn build_dispatcher_not_found(args: Vec<String>) -> AnyOutput { -//! AnyOutput::new(ErrorDispatcherNotFound::new(args)) +//! fn build_entry_fallback(args: Vec<String>) -> AnyOutput { +//! AnyOutput::new(EntryFallback::new(args)) //! } //! fn has_chain(any: &AnyOutput) -> bool { //! match any.member_id() { @@ -978,11 +978,11 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// The macros `gen_program!` automatically generates two fallback types that /// you can provide renderers for: /// - `ErrorRendererNotFound` — triggered when no matching renderer is found -/// - `ErrorDispatcherNotFound` — triggered when no matching dispatcher is found +/// - `EntryFallback` — triggered when no matching dispatcher is found /// /// ```rust,ignore /// #[renderer] -/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { +/// fn fallback_dispatcher_not_found(prev: EntryFallback) -> RenderResult { /// let mut result = RenderResult::new(); /// writeln!(result, "Unknown command: {}", prev.join(", ")); /// result @@ -1819,7 +1819,7 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { /// 1. **`pub type Next = ChainProcess<ProgramName>`** — A convenience type alias /// for use in chain function return types. /// 2. **`program_comp_gen!(...)`** (with `comp` feature) — Generates completion infrastructure. -/// 3. **`program_fallback_gen!(...)`** — Generates `ErrorRendererNotFound` and `ErrorDispatcherNotFound` types. +/// 3. **`program_fallback_gen!(...)`** — Generates `ErrorRendererNotFound` and `EntryFallback` types. /// 4. **`program_final_gen!(...)`** — Generates the program enum with: /// - An enum with all packed types as variants /// - `Display` implementation for the enum diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index fe44a49..8e3660c 100644 --- a/mingling_macros/src/systems/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -61,7 +61,7 @@ pub(crate) fn gen_dispatch_args_trie(entries: &[(String, String, String)]) -> To fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream { if nodes.is_empty() { return quote! { - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + return Ok(Self::build_entry_fallback(raw.to_vec())); }; } @@ -113,7 +113,7 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream arms.push(quote! { Some(#ch_char) => { #arm - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + return Ok(Self::build_entry_fallback(raw.to_vec())); } }); } else { @@ -135,7 +135,7 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream let match_body = quote! { match raw_chars.nth(0) { #(#arms)* - _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), + _ => return Ok(Self::build_entry_fallback(raw.to_vec())), } }; quote! { @@ -145,17 +145,17 @@ fn build_dispatch_body(nodes: &[(String, String)], depth: usize) -> TokenStream } else if !exact_checks.is_empty() { quote! { #(#exact_checks)* - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + return Ok(Self::build_entry_fallback(raw.to_vec())); } } else if arms.is_empty() { quote! { - return Ok(Self::build_dispatcher_not_found(raw.to_vec())); + return Ok(Self::build_entry_fallback(raw.to_vec())); } } else { quote! { match raw_chars.nth(0) { #(#arms)* - _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())), + _ => return Ok(Self::build_entry_fallback(raw.to_vec())), } } } |
