From 2c4138dd165adb6043cce6633741bab9595ad687 Mon Sep 17 00:00:00 2001 From: Justin Chen Date: Fri, 25 Aug 2023 01:28:24 +0800 Subject: feat: regex support (#411) * feat: add the regex support for the matcher * feat: add functions for searching with regex * feat: add commands for searching with regex and change the case sensitivity * docs: add explanations for the new feature --- Cargo.lock | 9 +++++---- Cargo.toml | 1 + config/joshuto.toml | 2 ++ docs/configuration/joshuto.toml.md | 2 ++ docs/configuration/keymap.toml.md | 5 +++++ src/commands/case_sensitivity.rs | 2 ++ src/commands/mod.rs | 1 + src/commands/search_regex.rs | 29 +++++++++++++++++++++++++++++ src/config/general/search_raw.rs | 12 ++++++++++++ src/config/option/search_option.rs | 2 ++ src/context/matcher.rs | 37 +++++++++++++++++++++++++++++++++++++ src/error/error_kind.rs | 2 ++ src/error/error_type.rs | 10 ++++++++++ src/key_command/command.rs | 3 +++ src/key_command/constants.rs | 1 + src/key_command/impl_appcommand.rs | 1 + src/key_command/impl_appexecute.rs | 1 + src/key_command/impl_comment.rs | 1 + src/key_command/impl_display.rs | 1 + src/key_command/impl_from_str.rs | 11 +++++++++++ 20 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/commands/search_regex.rs diff --git a/Cargo.lock b/Cargo.lock index 961db46..5f7ad03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,6 +469,7 @@ dependencies = [ "phf", "rand", "ratatui", + "regex", "rustyline", "serde", "serde_derive", @@ -847,9 +848,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", @@ -859,9 +860,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", diff --git a/Cargo.toml b/Cargo.toml index b285561..f97d570 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ whoami = "^1" xdg = "^2" walkdir = "^2" bitflags = { version = "^2", features = ["serde"] } +regex = "1.9.3" [dependencies.nix] version = "^0" diff --git a/config/joshuto.toml b/config/joshuto.toml index 9c875d4..9219ca0 100644 --- a/config/joshuto.toml +++ b/config/joshuto.toml @@ -44,6 +44,8 @@ string_case_sensitivity = "insensitive" # see above glob_case_sensitivity = "sensitive" # see above +regex_case_sensitivity = "sensitive" +# see above fzf_case_sensitivity = "insensitive" [tab] diff --git a/docs/configuration/joshuto.toml.md b/docs/configuration/joshuto.toml.md index 685ddde..31ba9f5 100644 --- a/docs/configuration/joshuto.toml.md +++ b/docs/configuration/joshuto.toml.md @@ -99,6 +99,8 @@ reverse = false string_case_sensitivity = "insensitive" # For glob matching glob_case_sensitivity = "sensitive" +# For regex matching +regex_case_sensitivity = "sensitive" # For matching with fzf fzf_case_sensitivity = "insensitive" diff --git a/docs/configuration/keymap.toml.md b/docs/configuration/keymap.toml.md index b926c1a..68a020b 100644 --- a/docs/configuration/keymap.toml.md +++ b/docs/configuration/keymap.toml.md @@ -319,6 +319,10 @@ function joshuto() { - `:search_glob *.png` +### `search_regex`: search the current directory via regex (exact match) + +- `:search_regex .+\.(jpg|png|gif)` + ### `search_next`: go to next search result in the current directory ### `search_prev`: go to previous search result in the current directory @@ -359,6 +363,7 @@ When disabling, the current “visual mode selection” is turned into normal se - Options - `--type=string`: change configurations of operations using substring matching - `--type=glob`: change configurations of operations using glob matching + - `--type=regex`: change configurations of operations using regex - `--type=fzf`: change configurations of operations using fzf - when no option is added, type is set to `string` by default - Value diff --git a/src/commands/case_sensitivity.rs b/src/commands/case_sensitivity.rs index c604b74..658c7fe 100644 --- a/src/commands/case_sensitivity.rs +++ b/src/commands/case_sensitivity.rs @@ -6,6 +6,7 @@ use crate::error::JoshutoResult; pub enum SetType { String, Glob, + Regex, Fzf, } @@ -19,6 +20,7 @@ pub fn set_case_sensitivity( match set_type { SetType::String => options.string_case_sensitivity = case_sensitivity, SetType::Glob => options.glob_case_sensitivity = case_sensitivity, + SetType::Regex => options.regex_case_sensitivity = case_sensitivity, SetType::Fzf => options.fzf_case_sensitivity = case_sensitivity, } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4753380..dcf3946 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -22,6 +22,7 @@ pub mod rename_file; pub mod search; pub mod search_fzf; pub mod search_glob; +pub mod search_regex; pub mod search_string; pub mod select; pub mod set_mode; diff --git a/src/commands/search_regex.rs b/src/commands/search_regex.rs new file mode 100644 index 0000000..90cc8f3 --- /dev/null +++ b/src/commands/search_regex.rs @@ -0,0 +1,29 @@ +use crate::context::{AppContext, MatchContext}; +use crate::error::JoshutoResult; + +use super::cursor_move; +use super::search; + +pub fn search_regex(context: &mut AppContext, pattern: &str) -> JoshutoResult { + let case_sensitivity = context + .config_ref() + .search_options_ref() + .regex_case_sensitivity; + + let search_context = MatchContext::new_regex(pattern, case_sensitivity)?; + + let curr_tab = &context.tab_context_ref().curr_tab_ref(); + let index = curr_tab.curr_list_ref().and_then(|c| c.get_index()); + + let offset = match index { + Some(index) => index + 1, + None => return Ok(()), + }; + + if let Some(new_index) = search::search_next_impl(curr_tab, &search_context, offset) { + cursor_move::cursor_move(context, new_index); + } + + context.set_search_context(search_context); + Ok(()) +} diff --git a/src/config/general/search_raw.rs b/src/config/general/search_raw.rs index cd9d397..672cd38 100644 --- a/src/config/general/search_raw.rs +++ b/src/config/general/search_raw.rs @@ -13,6 +13,10 @@ fn default_glob_case_sensitivity() -> String { "sensitive".to_string() } +fn default_regex_case_sensitivity() -> String { + "sensitive".to_string() +} + fn default_fzf_case_sensitivity() -> String { "insensitive".to_string() } @@ -25,6 +29,9 @@ pub struct SearchOptionRaw { #[serde(default = "default_glob_case_sensitivity")] pub glob_case_sensitivity: String, + #[serde(default = "default_regex_case_sensitivity")] + pub regex_case_sensitivity: String, + #[serde(default = "default_fzf_case_sensitivity")] pub fzf_case_sensitivity: String, } @@ -34,6 +41,7 @@ impl std::default::Default for SearchOptionRaw { SearchOptionRaw { string_case_sensitivity: default_string_case_sensitivity(), glob_case_sensitivity: default_glob_case_sensitivity(), + regex_case_sensitivity: default_regex_case_sensitivity(), fzf_case_sensitivity: default_fzf_case_sensitivity(), } } @@ -48,12 +56,16 @@ impl From for SearchOption { let glob_case_sensitivity = CaseSensitivity::from_str(raw.glob_case_sensitivity.as_str()) .unwrap_or(CaseSensitivity::Sensitive); + let regex_case_sensitivity = CaseSensitivity::from_str(raw.regex_case_sensitivity.as_str()) + .unwrap_or(CaseSensitivity::Sensitive); + let fzf_case_sensitivity = CaseSensitivity::from_str(raw.fzf_case_sensitivity.as_str()) .unwrap_or(CaseSensitivity::Insensitive); Self { string_case_sensitivity, glob_case_sensitivity, + regex_case_sensitivity, fzf_case_sensitivity, } } diff --git a/src/config/option/search_option.rs b/src/config/option/search_option.rs index 0fa889b..a7cac37 100644 --- a/src/config/option/search_option.rs +++ b/src/config/option/search_option.rs @@ -7,6 +7,7 @@ use crate::error::{JoshutoError, JoshutoErrorKind, JoshutoResult}; pub struct SearchOption { pub string_case_sensitivity: CaseSensitivity, pub glob_case_sensitivity: CaseSensitivity, + pub regex_case_sensitivity: CaseSensitivity, pub fzf_case_sensitivity: CaseSensitivity, } @@ -22,6 +23,7 @@ impl std::default::Default for SearchOption { Self { string_case_sensitivity: CaseSensitivity::Insensitive, glob_case_sensitivity: CaseSensitivity::Sensitive, + regex_case_sensitivity: CaseSensitivity::Sensitive, fzf_case_sensitivity: CaseSensitivity::Insensitive, } } diff --git a/src/context/matcher.rs b/src/context/matcher.rs index 31bda66..6c73161 100644 --- a/src/context/matcher.rs +++ b/src/context/matcher.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use globset::{GlobBuilder, GlobMatcher}; +use regex::{Regex, RegexBuilder}; use crate::config::option::CaseSensitivity; use crate::error::JoshutoResult; @@ -8,6 +9,7 @@ use crate::error::JoshutoResult; #[derive(Clone, Debug, Default)] pub enum MatchContext { Glob(GlobMatcher), + Regex(Regex), String { pattern: String, actual_case_sensitivity: CaseSensitivity, @@ -44,6 +46,32 @@ impl MatchContext { Ok(Self::Glob(glob)) } + pub fn new_regex(pattern: &str, case_sensitivity: CaseSensitivity) -> JoshutoResult { + let pattern_lower = pattern.to_lowercase(); + + let (pattern, actual_case_sensitivity) = match case_sensitivity { + CaseSensitivity::Insensitive => (pattern_lower.as_str(), CaseSensitivity::Insensitive), + CaseSensitivity::Sensitive => (pattern, CaseSensitivity::Sensitive), + // Determine the actual case sensitivity by whether an uppercase letter occurs. + CaseSensitivity::Smart => { + if pattern_lower == pattern { + (pattern_lower.as_str(), CaseSensitivity::Insensitive) + } else { + (pattern, CaseSensitivity::Sensitive) + } + } + }; + + let re = RegexBuilder::new(pattern) + .case_insensitive(matches!( + actual_case_sensitivity, + CaseSensitivity::Insensitive + )) + .build()?; + + Ok(Self::Regex(re)) + } + pub fn new_string(pattern: &str, case_sensitivity: CaseSensitivity) -> Self { let (pattern, actual_case_sensitivity) = match case_sensitivity { CaseSensitivity::Insensitive => (pattern.to_lowercase(), CaseSensitivity::Insensitive), @@ -68,6 +96,7 @@ impl MatchContext { pub fn is_match(&self, main: &str) -> bool { match self { Self::Glob(glob_matcher) => Self::is_match_glob(main, glob_matcher), + Self::Regex(regex) => Self::is_match_regex(main, regex), Self::String { pattern, actual_case_sensitivity, @@ -80,6 +109,13 @@ impl MatchContext { glob_matcher.is_match(main) } + fn is_match_regex(main: &str, regex: &Regex) -> bool { + match regex.find(main) { + Some(res) => res.range() == (0..main.len()), + None => false, + } + } + fn is_match_string( main: &str, pattern: &str, @@ -101,6 +137,7 @@ impl Display for MatchContext { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { Self::Glob(glob_matcher) => write!(f, "{}", glob_matcher.glob().glob()), + Self::Regex(regex) => write!(f, "{}", regex.as_str()), Self::String { pattern, .. } => write!(f, "{pattern}"), Self::None => Ok(()), } diff --git a/src/error/error_kind.rs b/src/error/error_kind.rs index 9afab66..a878b84 100644 --- a/src/error/error_kind.rs +++ b/src/error/error_kind.rs @@ -18,6 +18,8 @@ pub enum JoshutoErrorKind { Glob, + Regex, + InvalidParameters, UnrecognizedArgument, diff --git a/src/error/error_type.rs b/src/error/error_type.rs index a195ef6..ce96916 100644 --- a/src/error/error_type.rs +++ b/src/error/error_type.rs @@ -46,6 +46,16 @@ impl From for JoshutoError { } } +impl From for JoshutoError { + fn from(err: regex::Error) -> Self { + let cause = err.to_string(); + Self { + _kind: JoshutoErrorKind::Regex, + _cause: cause, + } + } +} + impl From for JoshutoError { fn from(err: std::env::VarError) -> Self { let cause = err.to_string(); diff --git a/src/key_command/command.rs b/src/key_command/command.rs index d4710eb..4469ebe 100644 --- a/src/key_command/command.rs +++ b/src/key_command/command.rs @@ -102,6 +102,9 @@ pub enum Command { SearchGlob { pattern: String, }, + SearchRegex { + pattern: String, + }, SearchString { pattern: String, }, diff --git a/src/key_command/constants.rs b/src/key_command/constants.rs index 4c808db..356997a 100644 --- a/src/key_command/constants.rs +++ b/src/key_command/constants.rs @@ -58,6 +58,7 @@ cmd_constants![ (CMD_SEARCH_STRING, "search"), (CMD_SEARCH_INCREMENTAL, "search_inc"), (CMD_SEARCH_GLOB, "search_glob"), + (CMD_SEARCH_REGEX, "search_regex"), (CMD_SEARCH_NEXT, "search_next"), (CMD_SEARCH_PREV, "search_prev"), (CMD_SELECT_FILES, "select"), diff --git a/src/key_command/impl_appcommand.rs b/src/key_command/impl_appcommand.rs index e6595a8..655ff64 100644 --- a/src/key_command/impl_appcommand.rs +++ b/src/key_command/impl_appcommand.rs @@ -61,6 +61,7 @@ impl AppCommand for Command { Self::SearchString { .. } => CMD_SEARCH_STRING, Self::SearchIncremental { .. } => CMD_SEARCH_INCREMENTAL, Self::SearchGlob { .. } => CMD_SEARCH_GLOB, + Self::SearchRegex { .. } => CMD_SEARCH_REGEX, Self::SearchNext => CMD_SEARCH_NEXT, Self::SearchPrev => CMD_SEARCH_PREV, diff --git a/src/key_command/impl_appexecute.rs b/src/key_command/impl_appexecute.rs index 1786e96..c4571ea 100644 --- a/src/key_command/impl_appexecute.rs +++ b/src/key_command/impl_appexecute.rs @@ -105,6 +105,7 @@ impl AppExecute for Command { } Self::TouchFile { file_name } => touch_file::touch_file(context, file_name), Self::SearchGlob { pattern } => search_glob::search_glob(context, pattern.as_str()), + Self::SearchRegex { pattern } => search_regex::search_regex(context, pattern.as_str()), Self::SearchString { pattern } => { search_string::search_string(context, pattern.as_str(), false); Ok(()) diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs index 85be947..9549621 100644 --- a/src/key_command/impl_comment.rs +++ b/src/key_command/impl_comment.rs @@ -91,6 +91,7 @@ impl CommandComment for Command { Self::SearchString { .. } => "Search", Self::SearchIncremental { .. } => "Search as you type", Self::SearchGlob { .. } => "Search with globbing", + Self::SearchRegex { .. } => "Search with regex", Self::SearchNext => "Next search entry", Self::SearchPrev => "Previous search entry", diff --git a/src/key_command/impl_display.rs b/src/key_command/impl_display.rs index b15fd43..d00c88e 100644 --- a/src/key_command/impl_display.rs +++ b/src/key_command/impl_display.rs @@ -46,6 +46,7 @@ impl std::fmt::Display for Command { Self::RenameFile { new_name } => write!(f, "{} {:?}", self.command(), new_name), Self::SearchGlob { pattern } => write!(f, "{} {}", self.command(), pattern), + Self::SearchRegex { pattern } => write!(f, "{} {}", self.command(), pattern), Self::SearchString { pattern } => write!(f, "{} {}", self.command(), pattern), Self::SelectFiles { pattern, options } => { write!(f, "{} {} {}", self.command(), pattern, options) diff --git a/src/key_command/impl_from_str.rs b/src/key_command/impl_from_str.rs index 0b76d38..1080198 100644 --- a/src/key_command/impl_from_str.rs +++ b/src/key_command/impl_from_str.rs @@ -320,6 +320,16 @@ impl std::str::FromStr for Command { pattern: arg.to_string(), }), } + } else if command == CMD_SEARCH_REGEX { + match arg { + "" => Err(JoshutoError::new( + JoshutoErrorKind::InvalidParameters, + format!("{}: Expected 1, got 0", command), + )), + arg => Ok(Self::SearchRegex { + pattern: arg.to_string(), + }), + } } else if command == CMD_SELECT_FILES { let mut options = SelectOption::default(); let mut pattern = ""; @@ -356,6 +366,7 @@ impl std::str::FromStr for Command { match arg.as_str() { "--type=string" => set_type = SetType::String, "--type=glob" => set_type = SetType::Glob, + "--type=regex" => set_type = SetType::Regex, "--type=fzf" => set_type = SetType::Fzf, s => value = s, } -- cgit v1.2.3