diff --git a/Cargo.lock b/Cargo.lock index 5ed80366a..377547010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -658,6 +673,17 @@ dependencies = [ "libc", ] +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "filetime" version = "0.2.27" @@ -1557,6 +1583,7 @@ dependencies = [ "directories", "dockworker", "errno 0.3.14", + "fancy-regex", "getch", "libc", "libproc", diff --git a/Cargo.toml b/Cargo.toml index 74de271ad..8e299c269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ tokio = { version = "1.52", optional = true, features = ["rt"] } toml = "1.1" unicode-width = "0.2" regex = "1.12" +fancy-regex = "0.16" [build-dependencies] anyhow = "1.0" diff --git a/src/main.rs b/src/main.rs index eb77c1c08..fddad48b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod columns; mod config; mod opt; mod process; +mod search_regex; mod style; mod term_info; mod util; diff --git a/src/search_regex.rs b/src/search_regex.rs new file mode 100644 index 000000000..e3df8bd14 --- /dev/null +++ b/src/search_regex.rs @@ -0,0 +1,60 @@ +use anyhow::Error; + +pub enum SearchRegex { + Fast(regex::Regex), + Fancy(fancy_regex::Regex), +} + +impl SearchRegex { + pub fn new(pattern: &str, ignore_case: bool) -> Result { + if let Ok(regex) = regex::RegexBuilder::new(pattern) + .case_insensitive(ignore_case) + .build() + { + return Ok(Self::Fast(regex)); + } + + let regex = fancy_regex::RegexBuilder::new(pattern) + .case_insensitive(ignore_case) + .build()?; + Ok(Self::Fancy(regex)) + } + + pub fn is_match(&self, text: &str) -> Result { + match self { + Self::Fast(regex) => Ok(regex.is_match(text)), + Self::Fancy(regex) => Ok(regex.is_match(text)?), + } + } +} + +#[cfg(test)] +mod tests { + use super::SearchRegex; + + #[test] + fn uses_fast_engine_for_standard_regex() { + let regex = SearchRegex::new("proc.*", false).unwrap(); + + assert!(matches!(regex, SearchRegex::Fast(_))); + assert!(regex.is_match("procs").unwrap()); + } + + #[test] + fn falls_back_to_fancy_engine_for_lookahead() { + let regex = SearchRegex::new("proc(?=s)", false).unwrap(); + + assert!(matches!(regex, SearchRegex::Fancy(_))); + assert!(regex.is_match("procs").unwrap()); + assert!(!regex.is_match("procx").unwrap()); + } + + #[test] + fn falls_back_to_fancy_engine_for_negative_lookahead() { + let regex = SearchRegex::new("proc(?!x)", false).unwrap(); + + assert!(matches!(regex, SearchRegex::Fancy(_))); + assert!(regex.is_match("procs").unwrap()); + assert!(!regex.is_match("procx").unwrap()); + } +} diff --git a/src/view.rs b/src/view.rs index c41badf9b..a8ed8b3a9 100644 --- a/src/view.rs +++ b/src/view.rs @@ -4,6 +4,7 @@ use crate::columns::*; use crate::config::*; use crate::opt::{ArgColorMode, ArgPagerMode}; use crate::process::collect_proc; +use crate::search_regex::SearchRegex; use crate::style::{apply_color, apply_style, color_to_column_style}; use crate::term_info::TermInfo; use crate::util::{ @@ -13,7 +14,6 @@ use crate::util::{ use anyhow::{Error, bail}; #[cfg(not(target_os = "windows"))] use pager::Pager; -use regex::RegexBuilder; use std::collections::HashMap; use std::time::Duration; @@ -52,10 +52,8 @@ impl View { // Adding the sort column to inserts if not already present match (&opt.sorta, &opt.sortd) { - (_, Some(col)) | (Some(col), _) => { - if !opt.insert.contains(col) { - opt.insert.push(col.clone()); - } + (_, Some(col)) | (Some(col), _) if !opt.insert.contains(col) => { + opt.insert.push(col.clone()); } _ => {} } @@ -263,9 +261,7 @@ impl View { ConfigSearchCase::Insensitive => true, ConfigSearchCase::Sensitive => false, }; - let regex = RegexBuilder::new(pattern) - .case_insensitive(ignore_case) - .build()?; + let regex = SearchRegex::new(pattern, ignore_case)?; Some(regex) } else { None @@ -316,7 +312,7 @@ impl View { } else if opt.keyword.is_empty() { true } else if let Some(regex) = ®ex { - View::search_regex(*pid, cols_searchable.as_slice(), regex) + View::search_regex(*pid, cols_searchable.as_slice(), regex)? } else { View::search( *pid, @@ -714,8 +710,13 @@ impl View { } } - fn search_regex(pid: i32, cols: &[&dyn Column], regex: ®ex::Regex) -> bool { - cols.iter().any(|c| regex.is_match(&c.display_json(pid))) + fn search_regex(pid: i32, cols: &[&dyn Column], regex: &SearchRegex) -> Result { + for c in cols { + if regex.is_match(&c.display_json(pid))? { + return Ok(true); + } + } + Ok(false) } #[cfg(not(any(target_os = "windows", any(target_os = "linux", target_os = "android"))))] diff --git a/src/watcher.rs b/src/watcher.rs index be99af074..21efe1267 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -1,12 +1,12 @@ use crate::Opt; use crate::config::*; +use crate::search_regex::SearchRegex; use crate::term_info::TermInfo; use crate::util::{get_theme, has_regex_syntax}; use crate::view::View; use anyhow::Error; use chrono::offset::Local; use getch::Getch; -use regex::RegexBuilder; use std::collections::HashMap; use std::sync::mpsc::{Receiver, Sender, channel}; use std::thread; @@ -221,10 +221,7 @@ impl Watcher { ConfigSearchCase::Insensitive => true, ConfigSearchCase::Sensitive => false, }; - match RegexBuilder::new(&candidate) - .case_insensitive(ignore_case) - .build() - { + match SearchRegex::new(&candidate, ignore_case) { Ok(_) => { opt.keyword = vec![candidate]; regex_error = None;