diff --git a/Cargo.lock b/Cargo.lock index a3d08f5..dbadf75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1806,6 +1806,7 @@ dependencies = [ "percentage", "rand", "rayon", + "regex", "reqwest", "serde", "url", @@ -1892,9 +1893,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" @@ -2539,9 +2540,21 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", @@ -2550,9 +2563,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" [[package]] name = "reqwest" diff --git a/Cargo.toml b/Cargo.toml index ca10ba1..120cada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ rand = { version = "0.8" } # transitive depedency, required for rand to support wasm getrandom = { version = "0.2", features = ["js"] } +regex = "1.10.0" + # client url = { version = "2", optional = true } diff --git a/src/app.rs b/src/app.rs index ff30ba7..8895805 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,6 +9,7 @@ use egui::{ }; use egui_extras::{Column, TableBuilder}; use percentage::{Percentage, PercentageInteger}; +use regex::{escape, Regex}; use serde::{Deserialize, Serialize}; use crate::data::{ @@ -110,10 +111,13 @@ struct SearchState { // Search parameters query: String, last_query: String, - include_collapsed_entries: bool, - last_include_collapsed_entries: bool, search_field: FieldID, last_search_field: FieldID, + whole_word: bool, + last_whole_word: bool, + last_word_regex: Option, + include_collapsed_entries: bool, + last_include_collapsed_entries: bool, last_view_interval: Option, // Cache of matching items @@ -1111,10 +1115,13 @@ impl SearchState { query: "".to_owned(), last_query: "".to_owned(), - include_collapsed_entries: false, - last_include_collapsed_entries: false, search_field: title_id, last_search_field: title_id, + whole_word: false, + last_whole_word: false, + last_word_regex: None, + include_collapsed_entries: false, + last_include_collapsed_entries: false, last_view_interval: None, result_set: BTreeSet::new(), @@ -1144,6 +1151,12 @@ impl SearchState { self.last_search_field = self.search_field; } + // Invalidate when the whole word setting changes. + if self.whole_word != self.last_whole_word { + invalidate = true; + self.last_whole_word = self.whole_word; + } + // Invalidate when EXCLUDING collapsed entries. (I.e., because the // searched set shrinks. Growing is ok because search is monotonic.) if self.include_collapsed_entries != self.last_include_collapsed_entries @@ -1160,12 +1173,24 @@ impl SearchState { } if invalidate { + if self.whole_word { + let regex_string = format!("\\b{}\\b", escape(&self.query)); + self.last_word_regex = Some(Regex::new(®ex_string).unwrap()); + } + self.clear(); } } fn is_string_match(&self, s: &str) -> bool { - s.contains(&self.query) + if self.whole_word { + let Some(regex) = &self.last_word_regex else { + unreachable!(); + }; + regex.is_match(s) + } else { + s.contains(&self.query) + } } fn is_field_match(&self, field: &Field) -> bool { @@ -1180,7 +1205,7 @@ impl SearchState { fn is_match(&self, item: &ItemMeta) -> bool { let field = self.search_field; if field == self.title_field { - item.title.contains(&self.query) + self.is_string_match(&item.title) } else if let Some((_, value)) = item.fields.iter().find(|(x, _)| *x == field) { self.is_field_match(value) } else { @@ -1583,6 +1608,10 @@ impl Window { } }); }); + ui.checkbox( + &mut self.config.search_state.whole_word, + "Match whole words only", + ); ui.checkbox( &mut self.config.search_state.include_collapsed_entries, "Include collapsed processors",