diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-04-12 01:06:24 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-04-12 01:06:24 +0800 |
| commit | 7c496151f571b872da523f4c46369751be6f4ca1 (patch) | |
| tree | e71d2aeb7a16f38996b9d68fe77259269ef25c8f | |
| parent | 5e10b03acb0312155d0d76e06d366bcf76cb9f27 (diff) | |
Add ShellContext helper methods for completion logic
| -rw-r--r-- | examples/example-completion/src/main.rs | 17 | ||||
| -rw-r--r-- | mingling/src/comp.rs | 157 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 17 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 9 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/shell_ctx.rs | 141 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp/suggest.rs | 7 | ||||
| -rw-r--r-- | mingling_core/src/program/flag.rs | 6 |
7 files changed, 336 insertions, 18 deletions
diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs index 06a0969..cbbb7a8 100644 --- a/examples/example-completion/src/main.rs +++ b/examples/example-completion/src/main.rs @@ -36,20 +36,21 @@ dispatcher!("fruit", FruitCommand => FruitEntry); #[completion(FruitEntry)] fn comp_fruit_command(ctx: &ShellContext) -> Suggest { - if ctx.current_word.starts_with("-") { - return suggest! { - "--name": "Fruit name", - "--type": "Fruit type" - }; - } - if ctx.previous_word == "--name" { + if ctx.filling_argument_first("--name") { return suggest!(); } - if ctx.previous_word == "--type" { + if ctx.filling_argument_first("--type") { return suggest! { "apple", "banana" }; } + if ctx.typing_argument() { + return suggest! { + "--name": "Fruit name", + "--type": "Fruit type" + } + .strip_typed_argument(ctx); + } return suggest!(); } diff --git a/mingling/src/comp.rs b/mingling/src/comp.rs new file mode 100644 index 0000000..7136ccc --- /dev/null +++ b/mingling/src/comp.rs @@ -0,0 +1,157 @@ +use std::collections::HashSet; + +use mingling_core::{Flag, ShellContext, Suggest}; + +pub struct ShellContextHelper { + ctx: ShellContext, +} + +impl ShellContextHelper { + /// Checks if a flag appears exactly once in the command line arguments. + /// + /// This method is useful for determining whether a flag should be processed + /// when it should only be applied once, even if it appears multiple times + /// in the command line. It returns `true` if the flag is present and + /// appears exactly once among all words in the shell context. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if either "--insert" or "-i" appears exactly once + /// if helper.filling_argument_first(["--insert", "-i"]) { + /// // Perform action that should only happen once, example: + /// // return suggest! { + /// // "A", "B", "C" + /// // } + /// } + /// ``` + pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool { + let flag = flag.into(); + if self.filling_argument(&flag) { + let mut flag_appears = 0; + for w in self.ctx.all_words.iter() { + for f in flag.iter() { + if *f == w { + flag_appears += 1; + } + } + } + if flag_appears < 2 { + return true; + } + } + return false; + } + + /// Checks if the previous word in the command line arguments matches any of the given flags. + /// + /// This method determines whether a flag is currently being processed + /// by checking the word immediately before the cursor position. It returns + /// `true` if the previous word matches any of the provided flag strings. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if the previous word is either "--file" or "-f" + /// if helper.filling_argument(["--file", "-f"]) { + /// // The user is likely expecting a file argument next, example: + /// // return suggest! { + /// // "src/main.rs", "Cargo.toml", "README.md" + /// // } + /// } + /// ``` + pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool { + for f in flag.into().iter() { + if self.ctx.previous_word == **f { + return true; + } + } + return false; + } + + /// Checks if the user is currently typing a flag argument. + /// + /// This method determines whether the current word being typed starts with + /// a dash (`-`), indicating that the user is likely in the process of + /// entering a command-line flag. It returns `true` if the current word + /// begins with a dash character. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if the user is typing a flag + /// if helper.typing_argument() { + /// // The user is likely entering a flag, example: + /// // return suggest! { + /// // "--help", "--version", "--verbose" + /// // } + /// } + /// ``` + pub fn typing_argument(&self) -> bool { + self.ctx.current_word.starts_with("-") + } + + /// Filters out already typed flag arguments from suggestion results. + /// + /// This method removes any suggestions that match flag arguments already present + /// in the command line. It is useful for preventing duplicate flag suggestions + /// when the user has already typed certain flags. The method processes both + /// regular suggestion sets and file completion suggestions differently. + pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest { + let typed = Self::get_typed_arguments(&self); + match suggest { + Suggest::Suggest(mut set) => { + set.retain(|item| !typed.contains(item.suggest())); + Suggest::Suggest(set) + } + Suggest::FileCompletion => Suggest::FileCompletion, + } + } + + /// Retrieves all flag arguments from the command line. + /// + /// This method collects all words in the shell context that start with a dash (`-`), + /// which typically represent command-line flags or options. It returns a vector + /// containing these flag strings, converted to owned `String` values. + pub fn get_typed_arguments(&self) -> HashSet<String> { + self.ctx + .all_words + .iter() + .filter(|word| word.starts_with("-")) + .map(|word| word.to_string()) + .collect() + } +} + +impl From<ShellContext> for ShellContextHelper { + fn from(ctx: ShellContext) -> Self { + Self { ctx } + } +} + +impl From<ShellContextHelper> for ShellContext { + fn from(helper: ShellContextHelper) -> Self { + helper.ctx + } +} diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 2e3f689..79a0626 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -119,20 +119,21 @@ pub mod example_basic {} /// /// #[completion(FruitEntry)] /// fn comp_fruit_command(ctx: &ShellContext) -> Suggest { -/// if ctx.current_word.starts_with("-") { -/// return suggest! { -/// "--name": "Fruit name", -/// "--type": "Fruit type" -/// }; -/// } -/// if ctx.previous_word == "--name" { +/// if ctx.filling_argument_first("--name") { /// return suggest!(); /// } -/// if ctx.previous_word == "--type" { +/// if ctx.filling_argument_first("--type") { /// return suggest! { /// "apple", "banana" /// }; /// } +/// if ctx.typing_argument() { +/// return suggest! { +/// "--name": "Fruit name", +/// "--type": "Fruit type" +/// } +/// .strip_typed_argument(ctx); +/// } /// return suggest!(); /// } /// diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 046f281..e2aa54a 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -59,6 +59,10 @@ //! `Mingling` provides detailed usage examples for your reference. //! See [Examples](_mingling_examples/index.html) +#[cfg(feature = "comp")] +mod comp; +mod example_docs; + // Re-export Core lib pub use mingling::*; pub use mingling_core as mingling; @@ -106,7 +110,10 @@ pub mod macros { /// derive macro Groupped pub use mingling_macros::Groupped; -mod example_docs; +#[cfg(feature = "comp")] +pub mod comp_tools { + pub use crate::comp::*; +} /// Example projects for `Mingling`, for learning how to use `Mingling` pub mod _mingling_examples { diff --git a/mingling_core/src/asset/comp/shell_ctx.rs b/mingling_core/src/asset/comp/shell_ctx.rs index 5ab0514..e5d095c 100644 --- a/mingling_core/src/asset/comp/shell_ctx.rs +++ b/mingling_core/src/asset/comp/shell_ctx.rs @@ -1,4 +1,6 @@ -use crate::ShellFlag; +use std::collections::HashSet; + +use crate::{Flag, ShellFlag, Suggest}; /// Context passed from the shell to the completion system, /// providing information about the current command line state @@ -92,6 +94,143 @@ impl TryFrom<Vec<String>> for ShellContext { } } +impl ShellContext { + /// Checks if a flag appears exactly once in the command line arguments. + /// + /// This method is useful for determining whether a flag should be processed + /// when it should only be applied once, even if it appears multiple times + /// in the command line. It returns `true` if the flag is present and + /// appears exactly once among all words in the shell context. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if either "--insert" or "-i" appears exactly once + /// if helper.filling_argument_first(["--insert", "-i"]) { + /// // Perform action that should only happen once, example: + /// // return suggest! { + /// // "A", "B", "C" + /// // } + /// } + /// ``` + pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool { + let flag = flag.into(); + if self.filling_argument(&flag) { + let mut flag_appears = 0; + for w in self.all_words.iter() { + for f in flag.iter() { + if *f == w { + flag_appears += 1; + } + } + } + if flag_appears < 2 { + return true; + } + } + return false; + } + + /// Checks if the previous word in the command line arguments matches any of the given flags. + /// + /// This method determines whether a flag is currently being processed + /// by checking the word immediately before the cursor position. It returns + /// `true` if the previous word matches any of the provided flag strings. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if the previous word is either "--file" or "-f" + /// if helper.filling_argument(["--file", "-f"]) { + /// // The user is likely expecting a file argument next, example: + /// // return suggest! { + /// // "src/main.rs", "Cargo.toml", "README.md" + /// // } + /// } + /// ``` + pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool { + for f in flag.into().iter() { + if self.previous_word == **f { + return true; + } + } + return false; + } + + /// Checks if the user is currently typing a flag argument. + /// + /// This method determines whether the current word being typed starts with + /// a dash (`-`), indicating that the user is likely in the process of + /// entering a command-line flag. It returns `true` if the current word + /// begins with a dash character. + /// + /// # Example + /// + /// ``` + /// # use mingling_core::ShellContext; + /// # use mingling_macros::suggest; + /// # use mingling::comp_tools::ShellContextHelper; + /// + /// let ctx = ShellContext::default(); + /// let helper = ShellContextHelper::from(ctx); + /// + /// // Check if the user is typing a flag + /// if helper.typing_argument() { + /// // The user is likely entering a flag, example: + /// // return suggest! { + /// // "--help", "--version", "--verbose" + /// // } + /// } + /// ``` + pub fn typing_argument(&self) -> bool { + self.current_word.starts_with("-") + } + + /// Filters out already typed flag arguments from suggestion results. + /// + /// This method removes any suggestions that match flag arguments already present + /// in the command line. It is useful for preventing duplicate flag suggestions + /// when the user has already typed certain flags. The method processes both + /// regular suggestion sets and file completion suggestions differently. + pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest { + let typed = Self::get_typed_arguments(&self); + match suggest { + Suggest::Suggest(mut set) => { + set.retain(|item| !typed.contains(item.suggest())); + Suggest::Suggest(set) + } + Suggest::FileCompletion => Suggest::FileCompletion, + } + } + + /// Retrieves all flag arguments from the command line. + /// + /// This method collects all words in the shell context that start with a dash (`-`), + /// which typically represent command-line flags or options. It returns a vector + /// containing these flag strings, converted to owned `String` values. + pub fn get_typed_arguments(&self) -> HashSet<String> { + self.all_words + .iter() + .filter(|word| word.starts_with("-")) + .map(|word| word.to_string()) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/mingling_core/src/asset/comp/suggest.rs b/mingling_core/src/asset/comp/suggest.rs index 81000d5..62844a7 100644 --- a/mingling_core/src/asset/comp/suggest.rs +++ b/mingling_core/src/asset/comp/suggest.rs @@ -1,5 +1,7 @@ use std::collections::BTreeSet; +use crate::ShellContext; + /// A completion suggestion that tells the shell how to perform completion. /// This can be either a set of specific suggestion items or a request for file completion. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] @@ -23,6 +25,11 @@ impl Suggest { pub fn file_comp() -> Self { Self::FileCompletion } + + /// Filters out already typed flag arguments from suggestion results. + pub fn strip_typed_argument(self, ctx: &ShellContext) -> Self { + ctx.strip_typed_argument(self) + } } impl<T> From<T> for Suggest diff --git a/mingling_core/src/program/flag.rs b/mingling_core/src/program/flag.rs index ed89c2a..ba3376c 100644 --- a/mingling_core/src/program/flag.rs +++ b/mingling_core/src/program/flag.rs @@ -46,6 +46,12 @@ pub struct Flag { vec: Vec<&'static str>, } +impl From<&Flag> for Flag { + fn from(value: &Flag) -> Self { + value.clone() + } +} + impl From<()> for Flag { fn from(_: ()) -> Self { Flag { vec: vec![] } |
