aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/comp/shell_ctx.rs
diff options
context:
space:
mode:
authorWeicao-CatilGrass <1992414357@qq.com>2026-05-09 13:21:01 +0800
committerWeicao-CatilGrass <1992414357@qq.com>2026-05-09 13:21:01 +0800
commitc0a29ccd2cd56e75c2b422b3cd9ca65d76a554da (patch)
tree26ce74a8f1138bed899d7896b48f1d5706ab96d4 /mingling_core/src/comp/shell_ctx.rs
parentc08ea7d8474335cadf13a2a7e45ca497fe375d90 (diff)
Move `comp` module from `asset` to crate root
Diffstat (limited to 'mingling_core/src/comp/shell_ctx.rs')
-rw-r--r--mingling_core/src/comp/shell_ctx.rs328
1 files changed, 328 insertions, 0 deletions
diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
new file mode 100644
index 0000000..dd2aa86
--- /dev/null
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -0,0 +1,328 @@
+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
+/// to guide how completions should be generated.
+#[derive(Default, Debug)]
+#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+pub struct ShellContext {
+ /// The full command line (-f / --command-line)
+ pub command_line: String,
+
+ /// Cursor position (-C / --cursor-position)
+ pub cursor_position: usize,
+
+ /// Current word (-w / --current-word)
+ pub current_word: String,
+
+ /// Previous word (-p / --previous-word)
+ pub previous_word: String,
+
+ /// Command name (-c / --command-name)
+ pub command_name: String,
+
+ /// Word index (-i / --word-index)
+ pub word_index: usize,
+
+ /// All words (-a / --all-words)
+ pub all_words: Vec<String>,
+
+ /// Flag to indicate completion context (-F / --shell-flag)
+ pub shell_flag: ShellFlag,
+}
+
+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;
+ }
+ }
+
+ // Extract values with defaults
+ let command_line = arg_map.get("-f").cloned().unwrap_or_default();
+ let cursor_position = arg_map
+ .get("-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")
+ .and_then(|s| s.parse().ok())
+ .unwrap_or_default();
+ let shell_flag = arg_map
+ .get("-F")
+ .cloned()
+ .map(ShellFlag::from)
+ .unwrap_or(ShellFlag::Other("unknown".to_string()));
+
+ let all_words = command_line
+ .split_whitespace()
+ .map(|s| s.replace('^', "-"))
+ .collect();
+
+ Ok(ShellContext {
+ command_line: command_line.replace('^', "-"),
+ cursor_position,
+ current_word: current_word.replace('^', "-"),
+ previous_word: previous_word.replace('^', "-"),
+ command_name: command_name.replace('^', "-"),
+ word_index,
+ all_words,
+ shell_flag,
+ })
+ }
+}
+
+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. On Windows, an empty current word is also
+ /// considered as typing a flag to accommodate shell behavior differences.
+ /// It returns `true` if the current word begins with a dash character.
+ ///
+ /// # Platform-specific behavior
+ ///
+ /// - **Windows**: Returns `true` if `current_word` is empty or starts with `-`
+ /// - **Other platforms**: Returns `true` only if `current_word` starts with `-`
+ ///
+ /// # 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 {
+ #[cfg(target_os = "windows")]
+ {
+ self.current_word.is_empty()
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ 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::*;
+
+ #[test]
+ fn test_try_from_full_args() {
+ let args = vec![
+ "-f".to_string(),
+ "git commit ^m 'test'".to_string(),
+ "-C".to_string(),
+ "12".to_string(),
+ "-w".to_string(),
+ "commit".to_string(),
+ "-p".to_string(),
+ "git".to_string(),
+ "-c".to_string(),
+ "git".to_string(),
+ "-i".to_string(),
+ "1".to_string(),
+ "-F".to_string(),
+ "bash".to_string(),
+ ];
+
+ let context = ShellContext::try_from(args).unwrap();
+ assert_eq!(context.command_line, "git commit -m 'test'");
+ assert_eq!(context.cursor_position, 12);
+ assert_eq!(context.current_word, "commit");
+ assert_eq!(context.previous_word, "git");
+ assert_eq!(context.command_name, "git");
+ assert_eq!(context.word_index, 1);
+ assert_eq!(context.all_words, vec!["git", "commit", "-m", "'test'"]);
+ assert!(matches!(context.shell_flag, ShellFlag::Bash));
+ }
+
+ #[test]
+ fn test_try_from_partial_args() {
+ let args = vec![
+ "-f".to_string(),
+ "ls ^la".to_string(),
+ "-C".to_string(),
+ "5".to_string(),
+ ];
+
+ let context = ShellContext::try_from(args).unwrap();
+ assert_eq!(context.command_line, "ls -la");
+ assert_eq!(context.cursor_position, 5);
+ assert_eq!(context.current_word, "");
+ assert_eq!(context.previous_word, "");
+ assert_eq!(context.command_name, "");
+ assert_eq!(context.word_index, 0);
+ assert_eq!(context.all_words, vec!["ls", "-la"]);
+ assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == "unknown"));
+ }
+
+ #[test]
+ fn test_try_from_empty_args() {
+ let args = vec![];
+ let context = ShellContext::try_from(args).unwrap();
+ assert_eq!(context.command_line, "");
+ assert_eq!(context.cursor_position, 0);
+ assert_eq!(context.current_word, "");
+ assert_eq!(context.previous_word, "");
+ assert_eq!(context.command_name, "");
+ assert_eq!(context.word_index, 0);
+ assert!(context.all_words.is_empty());
+ assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == "unknown"));
+ }
+
+ #[test]
+ fn test_try_from_flag_without_value() {
+ let args = vec!["-F".to_string()];
+ let context = ShellContext::try_from(args).unwrap();
+ assert!(matches!(context.shell_flag, ShellFlag::Other(ref s) if s == ""));
+ }
+
+ #[test]
+ fn test_all_words_splitting() {
+ let args = vec!["-f".to_string(), " cmd arg1 arg2 ".to_string()];
+ let context = ShellContext::try_from(args).unwrap();
+ assert_eq!(context.all_words, vec!["cmd", "arg1", "arg2"]);
+ }
+}