From ef35e4297c6d1fe4a101c7eded9d5e54ee165bf3 Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Sun, 23 Feb 2020 11:11:29 +0800 Subject: [PATCH 1/4] Feature: support case insensitive in exact mode --- src/engine.rs | 163 ++++++++++++++++++++++++++------------------------ src/score.rs | 10 ---- 2 files changed, 84 insertions(+), 89 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index afe56be0..e4ec2714 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2,7 +2,7 @@ use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; use crate::score::FuzzyAlgorithm; use crate::{score, SkimItem}; -use regex::Regex; +use regex::{escape, Regex}; use std::sync::Arc; lazy_static! { @@ -10,6 +10,19 @@ lazy_static! { static ref RE_OR: Regex = Regex::new(r" +\| +").unwrap(); } +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum CaseMatching { + Respect, + Ignore, + Smart, +} + +impl Default for CaseMatching { + fn default() -> Self { + CaseMatching::Smart + } +} + #[derive(Clone, Copy, PartialEq)] pub enum MatcherMode { Regex, @@ -198,55 +211,79 @@ struct ExactMatchingParam { prefix: bool, postfix: bool, inverse: bool, + case: CaseMatching, } #[derive(Debug)] struct ExactEngine { query: String, - query_chars: Vec, - param: ExactMatchingParam, + query_regex: Option, + inverse: bool, } impl ExactEngine { pub fn builder(query: &str, param: ExactMatchingParam) -> Self { + let case_sensitive = match param.case { + CaseMatching::Respect => true, + CaseMatching::Ignore => false, + CaseMatching::Smart => contains_upper(query), + }; + + let mut query_builder = String::new(); + if !case_sensitive { + query_builder.push_str("(?i)"); + } + + if param.prefix { + query_builder.push_str("^"); + } + + query_builder.push_str(&escape(query)); + + if param.postfix { + query_builder.push_str("$"); + } + + let query_regex = if query.is_empty() { + None + } else { + Regex::new(&query_builder).ok() + }; + ExactEngine { query: query.to_string(), - query_chars: query.chars().collect(), - param, + query_regex, + inverse: param.inverse, } } pub fn build(self) -> Self { self } +} - fn match_item_exact( - &self, - item: Arc, - // filter: , item_length> -> Option<(start, end)> - filter: impl Fn(&Option<((usize, usize), (usize, usize))>, usize) -> Option<(usize, usize)>, - ) -> Option { +impl MatchEngine for ExactEngine { + fn match_item(&self, item: Arc) -> Option { let mut matched_result = None; - let mut range_start = 0; - let mut range_end = 0; for &(start, end) in item.get_matching_ranges().as_ref() { - if self.query == "" { - matched_result = Some(((0, 0), (0, 0))); + if self.query_regex.is_none() { + matched_result = Some((0, 0)); break; } - matched_result = score::exact_match(&item.text()[start..end], &self.query); + matched_result = + score::regex_match(&item.text()[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); + + if self.inverse { + matched_result = matched_result.xor(Some((0, 0))) + } if matched_result.is_some() { - range_start = start; - range_end = end; break; } } - let (s, e) = filter(&matched_result, range_end - range_start)?; - - let (begin, end) = (s + range_start, e + range_start); + let (begin, end) = matched_result?; let score = (end - begin) as i64; let rank = build_rank(-score, item.get_index() as i64, begin as i64, end as i64); @@ -257,57 +294,12 @@ impl ExactEngine { .build(), ) } -} - -impl MatchEngine for ExactEngine { - fn match_item(&self, item: Arc) -> Option { - self.match_item_exact(item, |match_result, len| { - let match_range = match *match_result { - Some(((s1, e1), (s2, e2))) => { - if self.param.prefix && self.param.postfix { - if s1 == 0 && e2 == len { - Some((s1, e1)) - } else { - None - } - } else if self.param.prefix { - if s1 == 0 { - Some((s1, e1)) - } else { - None - } - } else if self.param.postfix { - if e2 == len { - Some((s2, e2)) - } else { - None - } - } else { - Some((s1, e1)) - } - } - None => None, - }; - - if self.param.inverse { - if match_range.is_some() { - None - } else { - Some((0, 0)) - } - } else { - match_range - } - }) - } fn display(&self) -> String { format!( - "(Exact|{}{}{}: {})", - if self.param.inverse { "!" } else { "" }, - if self.param.prefix { "^" } else { "" }, - if self.param.postfix { "$" } else { "" }, - self.query + "(Exact|{}{})", + if self.inverse { "!" } else { "" }, + self.query_regex.as_ref().map(|x| x.as_str()).unwrap_or("") ) } } @@ -443,6 +435,7 @@ impl MatchEngine for AndEngine { //------------------------------------------------------------------------------ pub struct EngineFactory {} + impl EngineFactory { pub fn build(query: &str, mode: MatcherMode, fuzzy_algorithm: FuzzyAlgorithm) -> Box { match mode { @@ -513,36 +506,48 @@ impl EngineFactory { } } +//============================================================================== +// utils +fn contains_upper(string: &str) -> bool { + for ch in string.chars() { + if ch.is_ascii_uppercase() { + return true; + } + } + false +} + #[cfg(test)] mod test { - use super::{EngineFactory, MatcherMode}; use crate::engine::FuzzyAlgorithm; + use super::{EngineFactory, MatcherMode}; + #[test] fn test_engine_factory() { let x = EngineFactory::build("'abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|: abc)"); + assert_eq!(x.display(), "(Exact|(?i)abc)"); let x = EngineFactory::build("^abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|^: abc)"); + assert_eq!(x.display(), "(Exact|(?i)^abc)"); let x = EngineFactory::build("abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|$: abc)"); + assert_eq!(x.display(), "(Exact|(?i)abc$)"); let x = EngineFactory::build("^abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|^$: abc)"); + assert_eq!(x.display(), "(Exact|(?i)^abc$)"); let x = EngineFactory::build("!abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!: abc)"); + assert_eq!(x.display(), "(Exact|!(?i)abc)"); let x = EngineFactory::build("!^abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!^: abc)"); + assert_eq!(x.display(), "(Exact|!(?i)^abc)"); let x = EngineFactory::build("!abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!$: abc)"); + assert_eq!(x.display(), "(Exact|!(?i)abc$)"); let x = EngineFactory::build("!^abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!^$: abc)"); + assert_eq!(x.display(), "(Exact|!(?i)^abc$)"); let x = EngineFactory::build( "'abc | def ^gh ij | kl mn", @@ -552,7 +557,7 @@ mod test { assert_eq!( x.display(), - "(And: (Or: (Exact|: abc), (Fuzzy: def)), (Exact|^: gh), (Or: (Fuzzy: ij), (Fuzzy: kl)), (Fuzzy: mn))" + "(And: (Or: (Exact|(?i)abc), (Fuzzy: def)), (Exact|(?i)^gh), (Or: (Fuzzy: ij), (Fuzzy: kl)), (Fuzzy: mn))" ); let x = EngineFactory::build( diff --git a/src/score.rs b/src/score.rs index ae677570..72548b9c 100644 --- a/src/score.rs +++ b/src/score.rs @@ -60,13 +60,3 @@ pub fn regex_match(choice: &str, pattern: &Option) -> Option<(usize, usiz None => None, } } - -// Pattern may appear in several places, return the first and last occurrence -pub fn exact_match(choice: &str, pattern: &str) -> Option<((usize, usize), (usize, usize))> { - // search from the start - let start_pos = choice.find(pattern)?; - let first_occur = (start_pos, start_pos + pattern.len()); - let last_pos = choice.rfind(pattern)?; - let last_occur = (last_pos, last_pos + pattern.len()); - Some((first_occur, last_occur)) -} From 5ba94a07546471cb62f97ddaa80800debe113d8d Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Sun, 23 Feb 2020 15:41:30 +0800 Subject: [PATCH 2/4] tmp: case insensitive + refactor engine --- Cargo.lock | 6 +- Cargo.toml | 5 +- src/{ => bin}/main.rs | 0 src/engine.rs | 579 ------------------------------------------ src/engine/all.rs | 43 ++++ src/engine/andor.rs | 139 ++++++++++ src/engine/exact.rs | 116 +++++++++ src/engine/factory.rs | 226 +++++++++++++++++ src/engine/fuzzy.rs | 163 ++++++++++++ src/engine/mod.rs | 7 + src/engine/regexp.rs | 85 +++++++ src/engine/util.rs | 21 ++ src/item.rs | 8 +- src/lib.rs | 60 ++++- src/matcher.rs | 57 ++--- src/model.rs | 53 ++-- src/prelude.rs | 2 +- src/score.rs | 62 ----- 18 files changed, 912 insertions(+), 720 deletions(-) rename src/{ => bin}/main.rs (100%) delete mode 100644 src/engine.rs create mode 100644 src/engine/all.rs create mode 100644 src/engine/andor.rs create mode 100644 src/engine/exact.rs create mode 100644 src/engine/factory.rs create mode 100644 src/engine/fuzzy.rs create mode 100644 src/engine/mod.rs create mode 100644 src/engine/regexp.rs create mode 100644 src/engine/util.rs delete mode 100644 src/score.rs diff --git a/Cargo.lock b/Cargo.lock index f3523ed3..b24ff7eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,7 +300,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "fuzzy-matcher" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -547,7 +547,7 @@ dependencies = [ "crossbeam 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuzzy-matcher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuzzy-matcher 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -782,7 +782,7 @@ dependencies = [ "checksum either 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c67353c641dc847124ea1902d69bd753dee9bb3beff9aa3662ecf86c971d1fac" "checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" -"checksum fuzzy-matcher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c20c3dd0480475a1e6da2f0d4dad7052d81386f56be9e23622e027c9240839b6" +"checksum fuzzy-matcher 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "75a03d6d8629fcd151ece9d3a7f59a87fc38a620ab0290bf2888c2ad73821170" "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" diff --git a/Cargo.toml b/Cargo.toml index b9589f1e..dad48b09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,7 @@ path = "src/lib.rs" [[bin]] name = "sk" -path = "src/main.rs" - +path = "src/bin/main.rs" [dependencies] nix = "0.14.0" @@ -32,7 +31,7 @@ time = "0.1.38" clap = "2.26.2" tuikit = "0.3.1" vte = "0.3.3" -fuzzy-matcher = "0.3.3" +fuzzy-matcher = "0.3.4" rayon = "1.0.3" derive_builder = "0.9" bitflags = "1.0.4" diff --git a/src/main.rs b/src/bin/main.rs similarity index 100% rename from src/main.rs rename to src/bin/main.rs diff --git a/src/engine.rs b/src/engine.rs deleted file mode 100644 index e4ec2714..00000000 --- a/src/engine.rs +++ /dev/null @@ -1,579 +0,0 @@ -///! matcher engine -use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; -use crate::score::FuzzyAlgorithm; -use crate::{score, SkimItem}; -use regex::{escape, Regex}; -use std::sync::Arc; - -lazy_static! { - static ref RE_AND: Regex = Regex::new(r"([^ |]+( +\| +[^ |]*)+)|( +)").unwrap(); - static ref RE_OR: Regex = Regex::new(r" +\| +").unwrap(); -} - -#[derive(Eq, PartialEq, Debug, Copy, Clone)] -pub enum CaseMatching { - Respect, - Ignore, - Smart, -} - -impl Default for CaseMatching { - fn default() -> Self { - CaseMatching::Smart - } -} - -#[derive(Clone, Copy, PartialEq)] -pub enum MatcherMode { - Regex, - Fuzzy, - Exact, -} - -// A match engine will execute the matching algorithm -pub trait MatchEngine: Sync + Send { - fn match_item(&self, item: Arc) -> Option; - fn display(&self) -> String; -} - -fn build_rank(score: i64, index: i64, begin: i64, end: i64) -> Rank { - Rank { - score, - index, - begin, - end, - } -} - -//------------------------------------------------------------------------------ -// Regular Expression engine -#[derive(Debug)] -struct RegexEngine { - query_regex: Option, -} - -impl RegexEngine { - pub fn builder(query: &str) -> Self { - RegexEngine { - query_regex: Regex::new(query).ok(), - } - } - - pub fn build(self) -> Self { - self - } -} - -impl MatchEngine for RegexEngine { - fn match_item(&self, item: Arc) -> Option { - let mut matched_result = None; - for &(start, end) in item.get_matching_ranges().as_ref() { - if self.query_regex.is_none() { - matched_result = Some((0, 0)); - break; - } - - matched_result = - score::regex_match(&item.text()[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); - - if matched_result.is_some() { - break; - } - } - - let (begin, end) = matched_result?; - let score = (end - begin) as i64; - let rank = build_rank(-score, item.get_index() as i64, begin as i64, end as i64); - - Some( - MatchedItem::builder(item) - .rank(rank) - .matched_range(MatchedRange::ByteRange(begin, end)) - .build(), - ) - } - - fn display(&self) -> String { - format!( - "(Regex: {})", - self.query_regex - .as_ref() - .map_or("".to_string(), |re| re.as_str().to_string()) - ) - } -} - -//------------------------------------------------------------------------------ -// Fuzzy engine -#[derive(Debug)] -struct FuzzyEngine { - query: String, - algorithm: FuzzyAlgorithm, -} - -impl FuzzyEngine { - pub fn builder(query: &str) -> Self { - FuzzyEngine { - query: query.to_string(), - algorithm: FuzzyAlgorithm::default(), - } - } - - pub fn algorithm(mut self, algorithm: FuzzyAlgorithm) -> Self { - self.algorithm = algorithm; - self - } - - pub fn build(self) -> Self { - self - } -} - -impl MatchEngine for FuzzyEngine { - fn match_item(&self, item: Arc) -> Option { - // iterate over all matching fields: - let mut matched_result = None; - for &(start, end) in item.get_matching_ranges().as_ref() { - matched_result = - score::fuzzy_match(&item.text()[start..end], &self.query, self.algorithm).map(|(s, vec)| { - if start != 0 { - let start_char = &item.text()[..start].chars().count(); - (s, vec.iter().map(|x| x + start_char).collect()) - } else { - (s, vec) - } - }); - - if matched_result.is_some() { - break; - } - } - - if matched_result == None { - return None; - } - - let (score, matched_range) = matched_result.unwrap(); - - let begin = *matched_range.get(0).unwrap_or(&0) as i64; - let end = *matched_range.last().unwrap_or(&0) as i64; - - let rank = build_rank(-score, item.get_index() as i64, begin, end); - - Some( - MatchedItem::builder(item) - .rank(rank) - .matched_range(MatchedRange::Chars(matched_range)) - .build(), - ) - } - - fn display(&self) -> String { - format!("(Fuzzy: {})", self.query) - } -} - -//------------------------------------------------------------------------------ -#[derive(Debug)] -struct MatchAllEngine {} - -impl MatchAllEngine { - pub fn builder() -> Self { - Self {} - } - - pub fn build(self) -> Self { - self - } -} - -impl MatchEngine for MatchAllEngine { - fn match_item(&self, item: Arc) -> Option { - let rank = build_rank(0, item.get_index() as i64, 0, 0); - - Some( - MatchedItem::builder(item) - .rank(rank) - .matched_range(MatchedRange::ByteRange(0, 0)) - .build(), - ) - } - - fn display(&self) -> String { - "Noop".to_string() - } -} - -//------------------------------------------------------------------------------ -// Exact engine -#[derive(Debug, Copy, Clone, Default)] -struct ExactMatchingParam { - prefix: bool, - postfix: bool, - inverse: bool, - case: CaseMatching, -} - -#[derive(Debug)] -struct ExactEngine { - query: String, - query_regex: Option, - inverse: bool, -} - -impl ExactEngine { - pub fn builder(query: &str, param: ExactMatchingParam) -> Self { - let case_sensitive = match param.case { - CaseMatching::Respect => true, - CaseMatching::Ignore => false, - CaseMatching::Smart => contains_upper(query), - }; - - let mut query_builder = String::new(); - if !case_sensitive { - query_builder.push_str("(?i)"); - } - - if param.prefix { - query_builder.push_str("^"); - } - - query_builder.push_str(&escape(query)); - - if param.postfix { - query_builder.push_str("$"); - } - - let query_regex = if query.is_empty() { - None - } else { - Regex::new(&query_builder).ok() - }; - - ExactEngine { - query: query.to_string(), - query_regex, - inverse: param.inverse, - } - } - - pub fn build(self) -> Self { - self - } -} - -impl MatchEngine for ExactEngine { - fn match_item(&self, item: Arc) -> Option { - let mut matched_result = None; - for &(start, end) in item.get_matching_ranges().as_ref() { - if self.query_regex.is_none() { - matched_result = Some((0, 0)); - break; - } - - matched_result = - score::regex_match(&item.text()[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); - - if self.inverse { - matched_result = matched_result.xor(Some((0, 0))) - } - - if matched_result.is_some() { - break; - } - } - - let (begin, end) = matched_result?; - let score = (end - begin) as i64; - let rank = build_rank(-score, item.get_index() as i64, begin as i64, end as i64); - - Some( - MatchedItem::builder(item) - .rank(rank) - .matched_range(MatchedRange::ByteRange(begin, end)) - .build(), - ) - } - - fn display(&self) -> String { - format!( - "(Exact|{}{})", - if self.inverse { "!" } else { "" }, - self.query_regex.as_ref().map(|x| x.as_str()).unwrap_or("") - ) - } -} - -//------------------------------------------------------------------------------ -// OrEngine, a combinator -struct OrEngine { - engines: Vec>, -} - -impl OrEngine { - pub fn builder(query: &str, mode: MatcherMode, fuzzy_algorithm: FuzzyAlgorithm) -> Self { - // mock - OrEngine { - engines: RE_OR - .split(query) - .map(|q| EngineFactory::build(q, mode, fuzzy_algorithm)) - .collect(), - } - } - - pub fn build(self) -> Self { - self - } -} - -impl MatchEngine for OrEngine { - fn match_item(&self, item: Arc) -> Option { - for engine in &self.engines { - let result = engine.match_item(Arc::clone(&item)); - if result.is_some() { - return result; - } - } - - None - } - - fn display(&self) -> String { - format!( - "(Or: {})", - self.engines.iter().map(|e| e.display()).collect::>().join(", ") - ) - } -} - -//------------------------------------------------------------------------------ -// AndEngine, a combinator -struct AndEngine { - engines: Vec>, -} - -impl AndEngine { - pub fn builder(query: &str, mode: MatcherMode, fuzzy_algorithm: FuzzyAlgorithm) -> Self { - let query_trim = query.trim_matches(|c| c == ' ' || c == '|'); - let mut engines = vec![]; - let mut last = 0; - for mat in RE_AND.find_iter(query_trim) { - let (start, end) = (mat.start(), mat.end()); - let term = &query_trim[last..start].trim_matches(|c| c == ' ' || c == '|'); - if !term.is_empty() { - engines.push(EngineFactory::build(term, mode, fuzzy_algorithm)); - } - - if !mat.as_str().trim().is_empty() { - engines.push(Box::new( - OrEngine::builder(mat.as_str().trim(), mode, fuzzy_algorithm).build(), - )); - } - last = end; - } - - let term = &query_trim[last..].trim_matches(|c| c == ' ' || c == '|'); - if !term.is_empty() { - engines.push(EngineFactory::build(term, mode, fuzzy_algorithm)); - } - - AndEngine { engines } - } - - pub fn build(self) -> Self { - self - } - - fn merge_matched_items(&self, items: Vec) -> MatchedItem { - let rank = items[0].rank; - let item = Arc::clone(&items[0].item); - let mut ranges = vec![]; - for item in items { - match item.matched_range { - Some(MatchedRange::ByteRange(..)) => { - ranges.extend(item.range_char_indices().unwrap()); - } - Some(MatchedRange::Chars(vec)) => { - ranges.extend(vec.iter()); - } - _ => {} - } - } - - ranges.sort(); - ranges.dedup(); - MatchedItem::builder(item) - .rank(rank) - .matched_range(MatchedRange::Chars(ranges)) - .build() - } -} - -impl MatchEngine for AndEngine { - fn match_item(&self, item: Arc) -> Option { - // mock - let mut results = vec![]; - for engine in &self.engines { - let result = engine.match_item(Arc::clone(&item))?; - results.push(result); - } - - if results.is_empty() { - None - } else { - Some(self.merge_matched_items(results)) - } - } - - fn display(&self) -> String { - format!( - "(And: {})", - self.engines.iter().map(|e| e.display()).collect::>().join(", ") - ) - } -} - -//------------------------------------------------------------------------------ -pub struct EngineFactory {} - -impl EngineFactory { - pub fn build(query: &str, mode: MatcherMode, fuzzy_algorithm: FuzzyAlgorithm) -> Box { - match mode { - MatcherMode::Regex => Box::new(RegexEngine::builder(query).build()), - MatcherMode::Fuzzy | MatcherMode::Exact => { - if query.contains(' ') { - Box::new(AndEngine::builder(query, mode, fuzzy_algorithm).build()) - } else { - EngineFactory::build_single(query, mode, fuzzy_algorithm) - } - } - } - } - - fn build_single(query: &str, mode: MatcherMode, fuzzy_algorithm: FuzzyAlgorithm) -> Box { - // 'abc => match exact "abc" - // ^abc => starts with "abc" - // abc$ => ends with "abc" - // ^abc$ => match exact "abc" - // !^abc => items not starting with "abc" - // !abc$ => items not ending with "abc" - // !^abc$ => not "abc" - - let mut query = query; - let mut exact = false; - let mut param = ExactMatchingParam::default(); - - if query.starts_with('\'') { - if mode == MatcherMode::Exact { - return Box::new(FuzzyEngine::builder(&query[1..]).algorithm(fuzzy_algorithm).build()); - } else { - return Box::new(ExactEngine::builder(&query[1..], param).build()); - } - } - - if query.starts_with('!') { - query = &query[1..]; - exact = true; - param.inverse = true; - } - - if query.is_empty() { - // if only "!" was provided, will still show all items - return Box::new(MatchAllEngine::builder().build()); - } - - if query.starts_with('^') { - query = &query[1..]; - exact = true; - param.prefix = true; - } - - if query.ends_with('$') { - query = &query[..(query.len() - 1)]; - exact = true; - param.postfix = true; - } - - if mode == MatcherMode::Exact { - exact = true; - } - - if exact { - Box::new(ExactEngine::builder(query, param).build()) - } else { - Box::new(FuzzyEngine::builder(query).algorithm(fuzzy_algorithm).build()) - } - } -} - -//============================================================================== -// utils -fn contains_upper(string: &str) -> bool { - for ch in string.chars() { - if ch.is_ascii_uppercase() { - return true; - } - } - false -} - -#[cfg(test)] -mod test { - use crate::engine::FuzzyAlgorithm; - - use super::{EngineFactory, MatcherMode}; - - #[test] - fn test_engine_factory() { - let x = EngineFactory::build("'abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|(?i)abc)"); - - let x = EngineFactory::build("^abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|(?i)^abc)"); - - let x = EngineFactory::build("abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|(?i)abc$)"); - - let x = EngineFactory::build("^abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|(?i)^abc$)"); - - let x = EngineFactory::build("!abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!(?i)abc)"); - - let x = EngineFactory::build("!^abc", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!(?i)^abc)"); - - let x = EngineFactory::build("!abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!(?i)abc$)"); - - let x = EngineFactory::build("!^abc$", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(Exact|!(?i)^abc$)"); - - let x = EngineFactory::build( - "'abc | def ^gh ij | kl mn", - MatcherMode::Fuzzy, - FuzzyAlgorithm::default(), - ); - - assert_eq!( - x.display(), - "(And: (Or: (Exact|(?i)abc), (Fuzzy: def)), (Exact|(?i)^gh), (Or: (Fuzzy: ij), (Fuzzy: kl)), (Fuzzy: mn))" - ); - - let x = EngineFactory::build( - "'abc | def ^gh ij | kl mn", - MatcherMode::Regex, - FuzzyAlgorithm::default(), - ); - assert_eq!(x.display(), "(Regex: 'abc | def ^gh ij | kl mn)"); - - let x = EngineFactory::build("abc ", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(And: (Fuzzy: abc))"); - - let x = EngineFactory::build("abc def", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(And: (Fuzzy: abc), (Fuzzy: def))"); - - let x = EngineFactory::build("abc | def", MatcherMode::Fuzzy, FuzzyAlgorithm::default()); - assert_eq!(x.display(), "(And: (Or: (Fuzzy: abc), (Fuzzy: def)))"); - } -} diff --git a/src/engine/all.rs b/src/engine/all.rs new file mode 100644 index 00000000..307feacd --- /dev/null +++ b/src/engine/all.rs @@ -0,0 +1,43 @@ +use std::fmt::{Display, Error, Formatter}; +use std::sync::Arc; + +use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; +use crate::MatchEngine; + +//------------------------------------------------------------------------------ +#[derive(Debug)] +pub struct MatchAllEngine {} + +impl MatchAllEngine { + pub fn builder() -> Self { + Self {} + } + + pub fn build(self) -> Self { + self + } +} + +impl MatchEngine for MatchAllEngine { + fn match_item(&self, item: Arc) -> Option { + let rank = Rank { + score: 0, + index: item.get_index() as i64, + begin: 0, + end: 0, + }; + + Some( + MatchedItem::builder(item) + .rank(rank) + .matched_range(MatchedRange::ByteRange(0, 0)) + .build(), + ) + } +} + +impl Display for MatchAllEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "Noop") + } +} diff --git a/src/engine/andor.rs b/src/engine/andor.rs new file mode 100644 index 00000000..dc28294d --- /dev/null +++ b/src/engine/andor.rs @@ -0,0 +1,139 @@ +use std::fmt::{Display, Error, Formatter}; +use std::sync::Arc; + +use crate::item::{ItemWrapper, MatchedItem, MatchedRange}; +use crate::{MatchEngine, MatchEngineFactory}; + +//------------------------------------------------------------------------------ +// OrEngine, a combinator +pub struct OrEngine { + engines: Vec>, +} + +impl OrEngine { + pub fn builder() -> Self { + Self { engines: vec![] } + } + + pub fn engine(mut self, engine: Box) -> Self { + self.engines.push(engine); + self + } + + pub fn engines(mut self, mut engines: Vec>) -> Self { + self.engines.append(&mut engines); + self + } + + pub fn build(self) -> Self { + self + } +} + +impl MatchEngine for OrEngine { + fn match_item(&self, item: Arc) -> Option { + for engine in &self.engines { + let result = engine.match_item(Arc::clone(&item)); + if result.is_some() { + return result; + } + } + + None + } +} + +impl Display for OrEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "(Or: {})", + self.engines + .iter() + .map(|e| format!("{}", e)) + .collect::>() + .join(", ") + ) + } +} + +//------------------------------------------------------------------------------ +// AndEngine, a combinator +pub struct AndEngine { + engines: Vec>, +} + +impl AndEngine { + pub fn builder() -> Self { + Self { engines: vec![] } + } + + pub fn engine(mut self, engine: Box) -> Self { + self.engines.push(engine); + self + } + + pub fn engines(mut self, mut engines: Vec>) -> Self { + self.engines.append(&mut engines); + self + } + + pub fn build(self) -> Self { + self + } + + fn merge_matched_items(&self, items: Vec) -> MatchedItem { + let rank = items[0].rank; + let item = Arc::clone(&items[0].item); + let mut ranges = vec![]; + for item in items { + match item.matched_range { + Some(MatchedRange::ByteRange(..)) => { + ranges.extend(item.range_char_indices().unwrap()); + } + Some(MatchedRange::Chars(vec)) => { + ranges.extend(vec.iter()); + } + _ => {} + } + } + + ranges.sort(); + ranges.dedup(); + MatchedItem::builder(item) + .rank(rank) + .matched_range(MatchedRange::Chars(ranges)) + .build() + } +} + +impl MatchEngine for AndEngine { + fn match_item(&self, item: Arc) -> Option { + // mock + let mut results = vec![]; + for engine in &self.engines { + let result = engine.match_item(Arc::clone(&item))?; + results.push(result); + } + + if results.is_empty() { + None + } else { + Some(self.merge_matched_items(results)) + } + } +} + +impl Display for AndEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "(And: {})", + self.engines + .iter() + .map(|e| format!("{}", e)) + .collect::>() + .join(", ") + ) + } +} diff --git a/src/engine/exact.rs b/src/engine/exact.rs new file mode 100644 index 00000000..095ff110 --- /dev/null +++ b/src/engine/exact.rs @@ -0,0 +1,116 @@ +use crate::engine::util::{contains_upper, regex_match}; +use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; +use crate::SkimItem; +use crate::{CaseMatching, MatchEngine, MatchEngineFactory}; +use regex::{escape, Regex}; +use std::fmt::{Display, Error, Formatter}; +use std::sync::Arc; + +//------------------------------------------------------------------------------ +// Exact engine +#[derive(Debug, Copy, Clone, Default)] +pub struct ExactMatchingParam { + pub prefix: bool, + pub postfix: bool, + pub inverse: bool, + pub case: CaseMatching, + __non_exhaustive: bool, +} + +#[derive(Debug)] +pub struct ExactEngine { + query: String, + query_regex: Option, + inverse: bool, +} + +impl ExactEngine { + pub fn builder(query: &str, param: ExactMatchingParam) -> Self { + let case_sensitive = match param.case { + CaseMatching::Respect => true, + CaseMatching::Ignore => false, + CaseMatching::Smart => contains_upper(query), + }; + + let mut query_builder = String::new(); + if !case_sensitive { + query_builder.push_str("(?i)"); + } + + if param.prefix { + query_builder.push_str("^"); + } + + query_builder.push_str(&escape(query)); + + if param.postfix { + query_builder.push_str("$"); + } + + let query_regex = if query.is_empty() { + None + } else { + Regex::new(&query_builder).ok() + }; + + ExactEngine { + query: query.to_string(), + query_regex, + inverse: param.inverse, + } + } + + pub fn build(self) -> Self { + self + } +} + +impl MatchEngine for ExactEngine { + fn match_item(&self, item: Arc) -> Option { + let mut matched_result = None; + for &(start, end) in item.get_matching_ranges().as_ref() { + if self.query_regex.is_none() { + matched_result = Some((0, 0)); + break; + } + + matched_result = + regex_match(&item.text()[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); + + if self.inverse { + matched_result = matched_result.xor(Some((0, 0))) + } + + if matched_result.is_some() { + break; + } + } + + let (begin, end) = matched_result?; + let score = (end - begin) as i64; + let rank = Rank { + score: -score, + index: item.get_index() as i64, + begin: begin as i64, + end: end as i64, + }; + + Some( + MatchedItem::builder(item) + .rank(rank) + .matched_range(MatchedRange::ByteRange(begin, end)) + .build(), + ) + } +} + +impl Display for ExactEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "(Exact|{}{})", + if self.inverse { "!" } else { "" }, + self.query_regex.as_ref().map(|x| x.as_str()).unwrap_or("") + ) + } +} diff --git a/src/engine/factory.rs b/src/engine/factory.rs new file mode 100644 index 00000000..a8227f29 --- /dev/null +++ b/src/engine/factory.rs @@ -0,0 +1,226 @@ +use crate::engine::all::MatchAllEngine; +use crate::engine::andor::{AndEngine, OrEngine}; +use crate::engine::exact::{ExactEngine, ExactMatchingParam}; +use crate::engine::fuzzy::{FuzzyAlgorithm, FuzzyEngine}; +use crate::engine::regexp::RegexEngine; +use crate::{CaseMatching, MatchEngine, MatchEngineFactory}; +use regex::Regex; + +lazy_static! { + static ref RE_AND: Regex = Regex::new(r"([^ |]+( +\| +[^ |]*)+)|( +)").unwrap(); + static ref RE_OR: Regex = Regex::new(r" +\| +").unwrap(); +} +//------------------------------------------------------------------------------ +// Exact engine factory +pub struct ExactOrFuzzyEngineFactory { + exact_mode: bool, + fuzzy_algorithm: FuzzyAlgorithm, +} + +impl ExactOrFuzzyEngineFactory { + pub fn builder() -> Self { + Self { + exact_mode: false, + fuzzy_algorithm: FuzzyAlgorithm::SkimV2, + } + } + + pub fn exact_mode(mut self, exact_mode: bool) -> Self { + self.exact_mode = exact_mode; + self + } + + pub fn fuzzy_algorithm(mut self, fuzzy_algorithm: FuzzyAlgorithm) -> Self { + self.fuzzy_algorithm = fuzzy_algorithm; + self + } + + pub fn build(self) -> Self { + self + } +} + +impl MatchEngineFactory for ExactOrFuzzyEngineFactory { + fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box { + // 'abc => match exact "abc" + // ^abc => starts with "abc" + // abc$ => ends with "abc" + // ^abc$ => match exact "abc" + // !^abc => items not starting with "abc" + // !abc$ => items not ending with "abc" + // !^abc$ => not "abc" + + let mut query = query; + let mut exact = false; + let mut param = ExactMatchingParam::default(); + param.case = case; + + if query.starts_with('\'') { + if self.exact_mode { + return Box::new( + FuzzyEngine::builder() + .query(&query[1..]) + .algorithm(self.fuzzy_algorithm) + .case(case) + .build(), + ); + } else { + exact = true; + query = &query[1..]; + } + } + + if query.starts_with('!') { + query = &query[1..]; + exact = true; + param.inverse = true; + } + + if query.is_empty() { + // if only "!" was provided, will still show all items + return Box::new(MatchAllEngine::builder().build()); + } + + if query.starts_with('^') { + query = &query[1..]; + exact = true; + param.prefix = true; + } + + if query.ends_with('$') { + query = &query[..(query.len() - 1)]; + exact = true; + param.postfix = true; + } + + if self.exact_mode { + exact = true; + } + + if exact { + Box::new(ExactEngine::builder(query, param).build()) + } else { + Box::new( + FuzzyEngine::builder() + .query(query) + .algorithm(self.fuzzy_algorithm) + .case(case) + .build(), + ) + } + } +} + +//------------------------------------------------------------------------------ +pub struct AndOrEngineFactory { + inner: Box, +} + +impl AndOrEngineFactory { + pub fn new(factory: impl MatchEngineFactory + 'static) -> Self { + Self { + inner: Box::new(factory), + } + } + + fn parse_or(&self, query: &str, case: CaseMatching) -> Box { + if query.trim().is_empty() { + self.inner.create_engine_with_case(query, case) + } else { + Box::new( + OrEngine::builder() + .engines(RE_OR.split(query).map(|q| self.parse_and(q, case)).collect()) + .build(), + ) + } + } + + fn parse_and(&self, query: &str, case: CaseMatching) -> Box { + let query_trim = query.trim_matches(|c| c == ' ' || c == '|'); + let mut engines = vec![]; + let mut last = 0; + for mat in RE_AND.find_iter(query_trim) { + let (start, end) = (mat.start(), mat.end()); + let term = query_trim[last..start].trim_matches(|c| c == ' ' || c == '|'); + if !term.is_empty() { + engines.push(self.inner.create_engine_with_case(term, case)); + } + + if !mat.as_str().trim().is_empty() { + engines.push(self.parse_or(mat.as_str().trim(), case)); + } + last = end; + } + + let term = query_trim[last..].trim_matches(|c| c == ' ' || c == '|'); + if !term.is_empty() { + engines.push(self.inner.create_engine_with_case(term, case)); + } + Box::new(AndEngine::builder().engines(engines).build()) + } +} + +impl MatchEngineFactory for AndOrEngineFactory { + fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box { + self.parse_or(query, case) + } +} + +//------------------------------------------------------------------------------ +pub struct RegexEngineFactory {} + +impl RegexEngineFactory { + pub fn new() -> Self { + Self {} + } +} + +impl MatchEngineFactory for RegexEngineFactory { + fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box { + Box::new(RegexEngine::builder(query, case).build()) + } +} + +mod test { + use super::*; + + #[test] + fn test_engine_factory() { + let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build(); + let x = exact_or_fuzzy.create_engine("'abc"); + assert_eq!(format!("{}", x), "(Exact|(?i)abc)"); + + let x = exact_or_fuzzy.create_engine("^abc"); + assert_eq!(format!("{}", x), "(Exact|(?i)^abc)"); + + let x = exact_or_fuzzy.create_engine("abc$"); + assert_eq!(format!("{}", x), "(Exact|(?i)abc$)"); + + let x = exact_or_fuzzy.create_engine("^abc$"); + assert_eq!(format!("{}", x), "(Exact|(?i)^abc$)"); + + let x = exact_or_fuzzy.create_engine("!abc"); + assert_eq!(format!("{}", x), "(Exact|!(?i)abc)"); + + let x = exact_or_fuzzy.create_engine("!^abc"); + assert_eq!(format!("{}", x), "(Exact|!(?i)^abc)"); + + let x = exact_or_fuzzy.create_engine("!abc$"); + assert_eq!(format!("{}", x), "(Exact|!(?i)abc$)"); + + let x = exact_or_fuzzy.create_engine("!^abc$"); + assert_eq!(format!("{}", x), "(Exact|!(?i)^abc$)"); + + let regex_factory = RegexEngineFactory::new(); + let and_or_factory = AndOrEngineFactory::new(exact_or_fuzzy); + + let x = and_or_factory.create_engine("'abc | def ^gh ij | kl mn"); + assert_eq!( + format!("{}", x), + "(Or: (And: (Exact|(?i)abc)), (And: (Fuzzy: def), (Exact|(?i)^gh), (Fuzzy: ij)), (And: (Fuzzy: kl), (Fuzzy: mn)))" + ); + + let x = regex_factory.create_engine("'abc | def ^gh ij | kl mn"); + assert_eq!(format!("{}", x), "(Regex: 'abc | def ^gh ij | kl mn)"); + } +} diff --git a/src/engine/fuzzy.rs b/src/engine/fuzzy.rs new file mode 100644 index 00000000..e812c6f6 --- /dev/null +++ b/src/engine/fuzzy.rs @@ -0,0 +1,163 @@ +use std::fmt::{Display, Error, Formatter}; +use std::sync::Arc; + +use fuzzy_matcher::clangd::ClangdMatcher; +use fuzzy_matcher::skim::{SkimMatcher, SkimMatcherV2}; +use fuzzy_matcher::FuzzyMatcher; + +use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; +use crate::SkimItem; +use crate::{CaseMatching, MatchEngine}; + +//------------------------------------------------------------------------------ +#[derive(Debug, Copy, Clone)] +pub enum FuzzyAlgorithm { + SkimV1, + SkimV2, + Clangd, +} + +impl FuzzyAlgorithm { + pub fn of(algorithm: &str) -> Self { + match algorithm.to_ascii_lowercase().as_ref() { + "skim_v1" => FuzzyAlgorithm::SkimV1, + "skim_v2" | "skim" => FuzzyAlgorithm::SkimV2, + "clangd" => FuzzyAlgorithm::Clangd, + _ => FuzzyAlgorithm::SkimV2, + } + } +} + +impl Default for FuzzyAlgorithm { + fn default() -> Self { + FuzzyAlgorithm::SkimV2 + } +} + +const BYTES_1M: usize = 1024 * 1024 * 1024; + +//------------------------------------------------------------------------------ +// Fuzzy engine +#[derive(Default)] +pub struct FuzzyEngineBuilder { + query: String, + case: CaseMatching, + algorithm: FuzzyAlgorithm, +} + +impl FuzzyEngineBuilder { + pub fn query(mut self, query: &str) -> Self { + self.query = query.to_string(); + self + } + + pub fn case(mut self, case: CaseMatching) -> Self { + self.case = case; + self + } + + pub fn algorithm(mut self, algorithm: FuzzyAlgorithm) -> Self { + self.algorithm = algorithm; + self + } + + pub fn build(self) -> FuzzyEngine { + let matcher: Box = match self.algorithm { + FuzzyAlgorithm::SkimV1 => Box::new(SkimMatcher::default()), + FuzzyAlgorithm::SkimV2 => { + let matcher = SkimMatcherV2::default().element_limit(BYTES_1M); + let matcher = match self.case { + CaseMatching::Respect => matcher.respect_case(), + CaseMatching::Ignore => matcher.ignore_case(), + CaseMatching::Smart => matcher.smart_case(), + }; + Box::new(matcher) + } + FuzzyAlgorithm::Clangd => { + let matcher = ClangdMatcher::default(); + let matcher = match self.case { + CaseMatching::Respect => matcher.respect_case(), + CaseMatching::Ignore => matcher.ignore_case(), + CaseMatching::Smart => matcher.smart_case(), + }; + Box::new(matcher) + } + }; + + FuzzyEngine { + matcher, + query: self.query, + } + } +} + +pub struct FuzzyEngine { + query: String, + matcher: Box, +} + +impl FuzzyEngine { + pub fn builder() -> FuzzyEngineBuilder { + FuzzyEngineBuilder::default() + } + + fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<(i64, Vec)> { + if pattern.is_empty() { + return Some((0, Vec::new())); + } else if choice.is_empty() { + return None; + } + + self.matcher.fuzzy_indices(choice, pattern) + } +} + +impl MatchEngine for FuzzyEngine { + fn match_item(&self, item: Arc) -> Option { + // iterate over all matching fields: + let mut matched_result = None; + for &(start, end) in item.get_matching_ranges().as_ref() { + matched_result = self.fuzzy_match(&item.text()[start..end], &self.query).map(|(s, vec)| { + if start != 0 { + let start_char = &item.text()[..start].chars().count(); + (s, vec.iter().map(|x| x + start_char).collect()) + } else { + (s, vec) + } + }); + + if matched_result.is_some() { + break; + } + } + + if matched_result == None { + return None; + } + + let (score, matched_range) = matched_result.unwrap(); + + let begin = *matched_range.get(0).unwrap_or(&0) as i64; + let end = *matched_range.last().unwrap_or(&0) as i64; + + let rank = Rank { + score: -score, + index: item.get_index() as i64, + begin, + end, + }; + + Some( + MatchedItem::builder(item) + .rank(rank) + .matched_range(MatchedRange::Chars(matched_range)) + .build(), + ) + } +} + +impl Display for FuzzyEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "(Fuzzy: {})", self.query) + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs new file mode 100644 index 00000000..52ba73ed --- /dev/null +++ b/src/engine/mod.rs @@ -0,0 +1,7 @@ +pub mod all; +pub mod andor; +pub mod exact; +pub mod factory; +pub mod fuzzy; +pub mod regexp; +mod util; diff --git a/src/engine/regexp.rs b/src/engine/regexp.rs new file mode 100644 index 00000000..bb630545 --- /dev/null +++ b/src/engine/regexp.rs @@ -0,0 +1,85 @@ +use std::fmt::{Display, Error, Formatter}; +use std::sync::Arc; + +use regex::Regex; + +use crate::engine::util::regex_match; +use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; +use crate::SkimItem; +use crate::{CaseMatching, MatchEngine, MatchEngineFactory}; + +//------------------------------------------------------------------------------ +// Regular Expression engine +#[derive(Debug)] +pub struct RegexEngine { + query_regex: Option, +} + +impl RegexEngine { + pub fn builder(query: &str, case: CaseMatching) -> Self { + let mut query_builder = String::new(); + + match case { + CaseMatching::Respect => {} + CaseMatching::Ignore => query_builder.push_str("(?i)"), + CaseMatching::Smart => {} + } + + query_builder.push_str(query); + + RegexEngine { + query_regex: Regex::new(&query_builder).ok(), + } + } + + pub fn build(self) -> Self { + self + } +} + +impl MatchEngine for RegexEngine { + fn match_item(&self, item: Arc) -> Option { + let mut matched_result = None; + for &(start, end) in item.get_matching_ranges().as_ref() { + if self.query_regex.is_none() { + matched_result = Some((0, 0)); + break; + } + + matched_result = + regex_match(&item.text()[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); + + if matched_result.is_some() { + break; + } + } + + let (begin, end) = matched_result?; + let score = (end - begin) as i64; + let rank = Rank { + score: -score, + index: item.get_index() as i64, + begin: begin as i64, + end: end as i64, + }; + + Some( + MatchedItem::builder(item) + .rank(rank) + .matched_range(MatchedRange::ByteRange(begin, end)) + .build(), + ) + } +} + +impl Display for RegexEngine { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "(Regex: {})", + self.query_regex + .as_ref() + .map_or("".to_string(), |re| re.as_str().to_string()) + ) + } +} diff --git a/src/engine/util.rs b/src/engine/util.rs new file mode 100644 index 00000000..0d64bad1 --- /dev/null +++ b/src/engine/util.rs @@ -0,0 +1,21 @@ +use crate::item::Rank; +use regex::Regex; + +pub fn regex_match(choice: &str, pattern: &Option) -> Option<(usize, usize)> { + match *pattern { + Some(ref pat) => { + let mat = pat.find(choice)?; + Some((mat.start(), mat.end())) + } + None => None, + } +} + +pub fn contains_upper(string: &str) -> bool { + for ch in string.chars() { + if ch.is_ascii_uppercase() { + return true; + } + } + false +} diff --git a/src/item.rs b/src/item.rs index e5417065..ab4b98a8 100644 --- a/src/item.rs +++ b/src/item.rs @@ -161,14 +161,14 @@ impl SkimItem for ItemWrapper { self.inner.text() } - fn output(&self) -> Cow { - self.inner.output() - } - fn preview(&self) -> ItemPreview { self.inner.preview() } + fn output(&self) -> Cow { + self.inner.output() + } + fn get_matching_ranges(&self) -> Cow<[(usize, usize)]> { self.inner.get_matching_ranges() } diff --git a/src/lib.rs b/src/lib.rs index 7003a113..bedcc98f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ extern crate log; #[macro_use] extern crate lazy_static; + mod ansi; mod engine; mod event; @@ -19,24 +20,28 @@ pub mod prelude; mod previewer; mod query; mod reader; -mod score; mod selection; mod spinlock; mod theme; mod util; +pub use crate::engine::fuzzy::FuzzyAlgorithm; pub use crate::ansi::AnsiString; +use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, RegexEngineFactory}; use crate::event::{EventReceiver, EventSender}; +use crate::input::Input; +use crate::item::{ItemWrapper, MatchedItem}; pub use crate::item_collector::*; +use crate::matcher::Matcher; use crate::model::Model; pub use crate::options::{SkimOptions, SkimOptionsBuilder}; pub use crate::output::SkimOutput; use crate::reader::Reader; -pub use crate::score::FuzzyAlgorithm; use crossbeam::channel::{Receiver, Sender}; use std::any::Any; use std::borrow::Cow; use std::env; +use std::fmt::Display; use std::sync::mpsc::channel; use std::sync::Arc; use std::thread; @@ -47,6 +52,7 @@ pub trait AsAny { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } + impl AsAny for T { fn as_any(&self) -> &dyn Any { self @@ -153,11 +159,41 @@ pub enum ItemPreview { Global, } +//============================================================================== +// A match engine will execute the matching algorithm + +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum CaseMatching { + Respect, + Ignore, + Smart, +} + +impl Default for CaseMatching { + fn default() -> Self { + CaseMatching::Smart + } +} + +pub trait MatchEngine: Sync + Send + Display { + fn match_item(&self, item: Arc) -> Option; +} + +pub trait MatchEngineFactory { + fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box; + fn create_engine(&self, query: &str) -> Box { + self.create_engine_with_case(query, CaseMatching::default()) + } +} + //------------------------------------------------------------------------------ pub type SkimItemSender = Sender>; pub type SkimItemReceiver = Receiver>; -pub struct Skim {} +pub struct Skim { + input: Input, +} + impl Skim { pub fn run_with(options: &SkimOptions, source: Option) -> Option { let min_height = options @@ -180,6 +216,7 @@ impl Skim { let mut input = input::Input::new(); input.parse_keymaps(&options.bind); input.parse_expect_keys(options.expect.as_ref().map(|x| &**x)); + let tx_clone = tx.clone(); let term_clone = term.clone(); let input_thread = thread::spawn(move || loop { @@ -210,8 +247,6 @@ impl Skim { } pub fn filter(options: &SkimOptions, source: Option) -> i32 { - use crate::engine::{EngineFactory, MatcherMode}; - let output_ending = if options.print0 { "\0" } else { "\n" }; let query = options.filter; let default_command = match env::var("SKIM_DEFAULT_COMMAND").as_ref().map(String::as_ref) { @@ -237,15 +272,16 @@ impl Skim { //------------------------------------------------------------------------------ // matcher - let matcher_mode = if options.regex { - MatcherMode::Regex - } else if options.exact { - MatcherMode::Exact + let engine_factory: Box = if options.regex { + Box::new(RegexEngineFactory::new()) } else { - MatcherMode::Fuzzy + let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder() + .fuzzy_algorithm(options.algorithm) + .exact_mode(options.exact) + .build(); + Box::new(AndOrEngineFactory::new(fuzzy_engine_factory)) }; - - let engine = EngineFactory::build(query, matcher_mode, options.algorithm); + let engine = engine_factory.create_engine_with_case(query, CaseMatching::default()); //------------------------------------------------------------------------------ // start diff --git a/src/matcher.rs b/src/matcher.rs index c897f5e9..8da5b153 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -1,15 +1,18 @@ -use crate::engine::EngineFactory; -pub use crate::engine::MatcherMode; -use crate::item::{ItemPool, MatchedItem}; -use crate::options::SkimOptions; -use crate::spinlock::SpinLock; -use crate::FuzzyAlgorithm; -use rayon::prelude::*; +use std::fmt::Display; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; use std::thread::JoinHandle; +use rayon::prelude::*; + +use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory}; +use crate::item::{ItemPool, ItemWrapper, MatchedItem}; +use crate::options::SkimOptions; +use crate::spinlock::SpinLock; +use crate::{CaseMatching, MatchEngineFactory}; + +//============================================================================== pub struct MatcherControl { stopped: Arc, processed: Arc, @@ -42,45 +45,35 @@ impl MatcherControl { } } +//============================================================================== pub struct Matcher { - mode: MatcherMode, + engine_factory: Box, + case_matching: CaseMatching, } impl Matcher { - pub fn new() -> Self { - Matcher { - mode: MatcherMode::Fuzzy, + pub fn builder(engine_factory: impl MatchEngineFactory + 'static) -> Self { + Self { + engine_factory: Box::new(engine_factory), + case_matching: CaseMatching::default(), } } - pub fn with_options(options: &SkimOptions) -> Self { - let mut matcher = Self::new(); - matcher.parse_options(&options); - matcher + pub fn case(mut self, case_matching: CaseMatching) -> Self { + self.case_matching = case_matching; + self } - fn parse_options(&mut self, options: &SkimOptions) { - if options.exact { - self.mode = MatcherMode::Exact; - } - - if options.regex { - self.mode = MatcherMode::Regex; - } + pub fn build(self) -> Self { + self } - pub fn run( - &self, - query: &str, - item_pool: Arc, - mode: Option, - fuzzy_algorithm: FuzzyAlgorithm, - callback: C, - ) -> MatcherControl + pub fn run(&self, query: &str, item_pool: Arc, callback: C) -> MatcherControl where C: Fn(Arc>>) + Send + 'static, { - let matcher_engine = EngineFactory::build(&query, mode.unwrap_or(self.mode), fuzzy_algorithm); + let matcher_engine = self.engine_factory.create_engine_with_case(query, self.case_matching); + debug!("engine: {}", matcher_engine); let stopped = Arc::new(AtomicBool::new(false)); let stopped_clone = stopped.clone(); let processed = Arc::new(AtomicUsize::new(0)); diff --git a/src/model.rs b/src/model.rs index dda83f30..7cf5b98b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::env; use std::mem; use std::process::Command; @@ -9,11 +10,12 @@ use regex::Regex; use timer::{Guard as TimerGuard, Timer}; use tuikit::prelude::{Event as TermEvent, *}; +use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, RegexEngineFactory}; use crate::event::{Event, EventHandler, EventReceiver, EventSender}; use crate::header::Header; use crate::input::parse_action_arg; use crate::item::{ItemPool, ItemWrapper}; -use crate::matcher::{Matcher, MatcherControl, MatcherMode}; +use crate::matcher::{Matcher, MatcherControl}; use crate::options::SkimOptions; use crate::output::SkimOutput; use crate::previewer::Previewer; @@ -24,7 +26,6 @@ use crate::spinlock::SpinLock; use crate::theme::ColorTheme; use crate::util::{inject_command, margin_string_to_size, parse_margin, InjectContext}; use crate::{FuzzyAlgorithm, SkimItem}; -use std::borrow::Cow; const REFRESH_DURATION: i64 = 100; const SPINNER_DURATION: u32 = 200; @@ -40,7 +41,11 @@ pub struct Model { query: Query, selection: Selection, num_options: usize, + + use_regex: bool, + regex_matcher: Matcher, matcher: Matcher, + term: Arc, item_pool: Arc, @@ -48,7 +53,6 @@ pub struct Model { rx: EventReceiver, tx: EventSender, - matcher_mode: Option, fuzzy_algorithm: FuzzyAlgorithm, reader_timer: Instant, matcher_timer: Instant, @@ -93,7 +97,11 @@ impl Model { .build(); let selection = Selection::with_options(options).theme(theme.clone()); - let matcher = Matcher::with_options(options); + let regex_engine = RegexEngineFactory::new(); + let regex_matcher = Matcher::builder(regex_engine).build(); + let fuzzy_engine = ExactOrFuzzyEngineFactory::builder().build(); + let matcher = Matcher::builder(AndOrEngineFactory::new(fuzzy_engine)).build(); + let item_pool = Arc::new(ItemPool::new().lines_to_reserve(options.header_lines)); let header = Header::empty() .with_options(options) @@ -111,6 +119,8 @@ impl Model { query, selection, num_options: 0, + use_regex: options.regex, + regex_matcher, matcher, term, item_pool, @@ -121,7 +131,6 @@ impl Model { matcher_timer: Instant::now(), reader_control: None, matcher_control: None, - matcher_mode: None, fuzzy_algorithm: FuzzyAlgorithm::default(), header, @@ -160,7 +169,7 @@ impl Model { } if options.regex { - self.matcher_mode = Some(MatcherMode::Regex); + self.use_regex = true; } self.fuzzy_algorithm = options.algorithm; @@ -276,11 +285,7 @@ impl Model { } fn act_rotate_mode(&mut self, env: &mut ModelEnv) { - if self.matcher_mode.is_none() { - self.matcher_mode = Some(MatcherMode::Regex); - } else { - self.matcher_mode = None; - } + self.use_regex = !self.use_regex; // restart matcher if let Some(ctrl) = self.matcher_control.take() { @@ -560,17 +565,17 @@ impl Model { // send heart beat (so that heartbeat/refresh is triggered) let _ = self.tx.send(Event::EvHeartBeat); + let matcher = if self.use_regex { + &self.regex_matcher + } else { + &self.matcher + }; + let tx = self.tx.clone(); - let new_matcher_control = self.matcher.run( - &query, - self.item_pool.clone(), - self.matcher_mode, - self.fuzzy_algorithm, - move |_| { - // notify refresh immediately - let _ = tx.send(Event::EvHeartBeat); - }, - ); + let new_matcher_control = matcher.run(&query, self.item_pool.clone(), move |_| { + // notify refresh immediately + let _ = tx.send(Event::EvHeartBeat); + }); self.matcher_control.replace(new_matcher_control); } @@ -581,10 +586,10 @@ impl Model { F: Fn(Box + '_>) -> R, { let total = self.item_pool.len(); - let matcher_mode = if self.matcher_mode.is_none() { - "".to_string() - } else { + let matcher_mode = if self.use_regex { "RE".to_string() + } else { + "".to_string() }; let matched = self.num_options + self.matcher_control.as_ref().map(|c| c.get_num_matched()).unwrap_or(0); diff --git a/src/prelude.rs b/src/prelude.rs index 0b8aef39..fa0c2dce 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,7 +2,7 @@ pub use crate::ansi::AnsiString; pub use crate::item_collector::*; pub use crate::options::{SkimOptions, SkimOptionsBuilder}; pub use crate::output::SkimOutput; -pub use crate::score::FuzzyAlgorithm; +pub use crate::FuzzyAlgorithm; pub use crate::{AsAny, ItemPreview, Skim, SkimItem, SkimItemReceiver, SkimItemSender}; pub use crossbeam::channel::{bounded, unbounded, Receiver, Sender}; pub use std::borrow::Cow; diff --git a/src/score.rs b/src/score.rs deleted file mode 100644 index 72548b9c..00000000 --- a/src/score.rs +++ /dev/null @@ -1,62 +0,0 @@ -///! score is responsible for calculating the scores of the similarity between -///! the query and the choice. -use fuzzy_matcher::clangd::ClangdMatcher; -use fuzzy_matcher::skim::{SkimMatcher, SkimMatcherV2}; -use fuzzy_matcher::FuzzyMatcher; -use regex::Regex; - -#[derive(Debug, Copy, Clone)] -pub enum FuzzyAlgorithm { - SkimV1, - SkimV2, - Clangd, -} - -impl FuzzyAlgorithm { - pub fn of(algorithm: &str) -> Self { - match algorithm.to_ascii_lowercase().as_ref() { - "skim_v1" => FuzzyAlgorithm::SkimV1, - "skim_v2" | "skim" => FuzzyAlgorithm::SkimV2, - "clangd" => FuzzyAlgorithm::Clangd, - _ => FuzzyAlgorithm::SkimV2, - } - } -} - -impl Default for FuzzyAlgorithm { - fn default() -> Self { - FuzzyAlgorithm::SkimV2 - } -} - -const BYTES_1M: usize = 1024 * 1024 * 1024; - -lazy_static! { - static ref SKIM_V1: SkimMatcher = SkimMatcher::default(); - static ref SKIM_V2: SkimMatcherV2 = SkimMatcherV2::default().element_limit(BYTES_1M); - static ref CLANGD: ClangdMatcher = ClangdMatcher::default(); -} - -pub fn fuzzy_match(choice: &str, pattern: &str, fuzzy_algorithm: FuzzyAlgorithm) -> Option<(i64, Vec)> { - if pattern.is_empty() { - return Some((0, Vec::new())); - } else if choice.is_empty() { - return None; - } - - match fuzzy_algorithm { - FuzzyAlgorithm::SkimV1 => SKIM_V1.fuzzy_indices(choice, pattern), - FuzzyAlgorithm::SkimV2 => SKIM_V2.fuzzy_indices(choice, pattern), - FuzzyAlgorithm::Clangd => CLANGD.fuzzy_indices(choice, pattern), - } -} - -pub fn regex_match(choice: &str, pattern: &Option) -> Option<(usize, usize)> { - match *pattern { - Some(ref pat) => { - let mat = pat.find(choice)?; - Some((mat.start(), mat.end())) - } - None => None, - } -} From 98243439777d969c2e861c99c0b5c9856c6013a8 Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Sun, 23 Feb 2020 18:14:50 +0800 Subject: [PATCH 3/4] Fix #219: support case insensitive matching Also support custom engine --- src/bin/main.rs | 10 +++++++++- src/engine/andor.rs | 12 +----------- src/engine/exact.rs | 2 +- src/engine/factory.rs | 3 +-- src/engine/regexp.rs | 2 +- src/engine/util.rs | 1 - src/lib.rs | 6 +----- src/matcher.rs | 12 +++++------- src/model.rs | 17 +++++++++++++---- src/options.rs | 11 +++++++++-- 10 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index a02e1d36..6986fef3 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -8,7 +8,7 @@ extern crate time; use clap::{App, Arg, ArgMatches}; use nix::unistd::isatty; use skim::{ - read_and_collect_from_command, CollectorInput, CollectorOption, FuzzyAlgorithm, Skim, SkimOptions, + read_and_collect_from_command, CaseMatching, CollectorInput, CollectorOption, FuzzyAlgorithm, Skim, SkimOptions, SkimOptionsBuilder, }; use std::env; @@ -37,6 +37,8 @@ Usage: sk [options] --regex use regex instead of fuzzy match --algo=TYPE Fuzzy matching algorithm: [skim_v1|skim_v2|clangd] (default: skim_v2) + --case [respect,ignore,smart] (default: smart) + case sensitive or not Interface -b, --bind KEYBINDS comma seperated keybindings, in KEY:ACTION @@ -190,6 +192,7 @@ fn real_main() -> i32 { .arg(Arg::with_name("reverse").long("reverse").multiple(true)) .arg(Arg::with_name("algorithm").long("algo").multiple(true).takes_value(true).default_value("skim_v2")) + .arg(Arg::with_name("case").long("case").multiple(true).takes_value(true).default_value("smart")) .arg(Arg::with_name("literal").long("literal").multiple(true)) .arg(Arg::with_name("cycle").long("cycle").multiple(true)) .arg(Arg::with_name("no-hscroll").long("no-hscroll").multiple(true)) @@ -334,6 +337,11 @@ fn parse_options<'a>(options: &'a ArgMatches) -> SkimOptions<'a> { .algorithm(FuzzyAlgorithm::of( options.values_of("algorithm").and_then(|vals| vals.last()).unwrap(), )) + .case(match options.value_of("case") { + Some("smart") => CaseMatching::Smart, + Some("ignore") => CaseMatching::Ignore, + _ => CaseMatching::Respect, + }) .build() .unwrap() } diff --git a/src/engine/andor.rs b/src/engine/andor.rs index dc28294d..dc8680a5 100644 --- a/src/engine/andor.rs +++ b/src/engine/andor.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Error, Formatter}; use std::sync::Arc; use crate::item::{ItemWrapper, MatchedItem, MatchedRange}; -use crate::{MatchEngine, MatchEngineFactory}; +use crate::MatchEngine; //------------------------------------------------------------------------------ // OrEngine, a combinator @@ -15,11 +15,6 @@ impl OrEngine { Self { engines: vec![] } } - pub fn engine(mut self, engine: Box) -> Self { - self.engines.push(engine); - self - } - pub fn engines(mut self, mut engines: Vec>) -> Self { self.engines.append(&mut engines); self @@ -68,11 +63,6 @@ impl AndEngine { Self { engines: vec![] } } - pub fn engine(mut self, engine: Box) -> Self { - self.engines.push(engine); - self - } - pub fn engines(mut self, mut engines: Vec>) -> Self { self.engines.append(&mut engines); self diff --git a/src/engine/exact.rs b/src/engine/exact.rs index 095ff110..c097e329 100644 --- a/src/engine/exact.rs +++ b/src/engine/exact.rs @@ -1,7 +1,7 @@ use crate::engine::util::{contains_upper, regex_match}; use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; use crate::SkimItem; -use crate::{CaseMatching, MatchEngine, MatchEngineFactory}; +use crate::{CaseMatching, MatchEngine}; use regex::{escape, Regex}; use std::fmt::{Display, Error, Formatter}; use std::sync::Arc; diff --git a/src/engine/factory.rs b/src/engine/factory.rs index a8227f29..df7c0687 100644 --- a/src/engine/factory.rs +++ b/src/engine/factory.rs @@ -182,10 +182,9 @@ impl MatchEngineFactory for RegexEngineFactory { } mod test { - use super::*; - #[test] fn test_engine_factory() { + use super::*; let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build(); let x = exact_or_fuzzy.create_engine("'abc"); assert_eq!(format!("{}", x), "(Exact|(?i)abc)"); diff --git a/src/engine/regexp.rs b/src/engine/regexp.rs index bb630545..482b7bc0 100644 --- a/src/engine/regexp.rs +++ b/src/engine/regexp.rs @@ -6,7 +6,7 @@ use regex::Regex; use crate::engine::util::regex_match; use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; use crate::SkimItem; -use crate::{CaseMatching, MatchEngine, MatchEngineFactory}; +use crate::{CaseMatching, MatchEngine}; //------------------------------------------------------------------------------ // Regular Expression engine diff --git a/src/engine/util.rs b/src/engine/util.rs index 0d64bad1..63b18d92 100644 --- a/src/engine/util.rs +++ b/src/engine/util.rs @@ -1,4 +1,3 @@ -use crate::item::Rank; use regex::Regex; pub fn regex_match(choice: &str, pattern: &Option) -> Option<(usize, usize)> { diff --git a/src/lib.rs b/src/lib.rs index bedcc98f..8434dd4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,10 +29,8 @@ pub use crate::engine::fuzzy::FuzzyAlgorithm; pub use crate::ansi::AnsiString; use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, RegexEngineFactory}; use crate::event::{EventReceiver, EventSender}; -use crate::input::Input; use crate::item::{ItemWrapper, MatchedItem}; pub use crate::item_collector::*; -use crate::matcher::Matcher; use crate::model::Model; pub use crate::options::{SkimOptions, SkimOptionsBuilder}; pub use crate::output::SkimOutput; @@ -190,9 +188,7 @@ pub trait MatchEngineFactory { pub type SkimItemSender = Sender>; pub type SkimItemReceiver = Receiver>; -pub struct Skim { - input: Input, -} +pub struct Skim {} impl Skim { pub fn run_with(options: &SkimOptions, source: Option) -> Option { diff --git a/src/matcher.rs b/src/matcher.rs index 8da5b153..c7a8c93a 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -1,4 +1,3 @@ -use std::fmt::Display; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; @@ -6,11 +5,10 @@ use std::thread::JoinHandle; use rayon::prelude::*; -use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory}; -use crate::item::{ItemPool, ItemWrapper, MatchedItem}; -use crate::options::SkimOptions; +use crate::item::{ItemPool, MatchedItem}; use crate::spinlock::SpinLock; use crate::{CaseMatching, MatchEngineFactory}; +use std::rc::Rc; //============================================================================== pub struct MatcherControl { @@ -47,14 +45,14 @@ impl MatcherControl { //============================================================================== pub struct Matcher { - engine_factory: Box, + engine_factory: Rc, case_matching: CaseMatching, } impl Matcher { - pub fn builder(engine_factory: impl MatchEngineFactory + 'static) -> Self { + pub fn builder(engine_factory: Rc) -> Self { Self { - engine_factory: Box::new(engine_factory), + engine_factory, case_matching: CaseMatching::default(), } } diff --git a/src/model.rs b/src/model.rs index 7cf5b98b..ab23d0d1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::env; use std::mem; use std::process::Command; +use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -25,7 +26,7 @@ use crate::selection::Selection; use crate::spinlock::SpinLock; use crate::theme::ColorTheme; use crate::util::{inject_command, margin_string_to_size, parse_margin, InjectContext}; -use crate::{FuzzyAlgorithm, SkimItem}; +use crate::{FuzzyAlgorithm, MatchEngineFactory, SkimItem}; const REFRESH_DURATION: i64 = 100; const SPINNER_DURATION: u32 = 200; @@ -97,10 +98,18 @@ impl Model { .build(); let selection = Selection::with_options(options).theme(theme.clone()); - let regex_engine = RegexEngineFactory::new(); + let regex_engine: Rc = Rc::new(RegexEngineFactory::new()); let regex_matcher = Matcher::builder(regex_engine).build(); - let fuzzy_engine = ExactOrFuzzyEngineFactory::builder().build(); - let matcher = Matcher::builder(AndOrEngineFactory::new(fuzzy_engine)).build(); + + let matcher = if let Some(engine_factory) = options.engine_factory.as_ref() { + // use provided engine + Matcher::builder(engine_factory.clone()).case(options.case).build() + } else { + let fuzzy_engine_factory: Rc = Rc::new(AndOrEngineFactory::new( + ExactOrFuzzyEngineFactory::builder().exact_mode(options.exact).build(), + )); + Matcher::builder(fuzzy_engine_factory).case(options.case).build() + }; let item_pool = Arc::new(ItemPool::new().lines_to_reserve(options.header_lines)); let header = Header::empty() diff --git a/src/options.rs b/src/options.rs index 59e85920..0cff1001 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,7 +1,10 @@ -use crate::FuzzyAlgorithm; +use std::rc::Rc; + use derive_builder::Builder; -#[derive(Debug, Builder)] +use crate::{CaseMatching, FuzzyAlgorithm, MatchEngineFactory}; + +#[derive(Builder)] #[builder(build_fn(name = "final_build"))] #[builder(default)] pub struct SkimOptions<'a> { @@ -45,6 +48,8 @@ pub struct SkimOptions<'a> { pub layout: &'a str, pub filter: &'a str, pub algorithm: FuzzyAlgorithm, + pub case: CaseMatching, + pub engine_factory: Option>, } impl<'a> Default for SkimOptions<'a> { @@ -90,6 +95,8 @@ impl<'a> Default for SkimOptions<'a> { layout: "", filter: "", algorithm: FuzzyAlgorithm::default(), + case: CaseMatching::default(), + engine_factory: None, } } } From 3edc75d75ec8d0b7db08f1eb8e2056a962887bc1 Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Sun, 23 Feb 2020 18:42:25 +0800 Subject: [PATCH 4/4] add tests for case folding --- test/test_skim.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/test_skim.py b/test/test_skim.py index 3c54f5cb..55b4da82 100644 --- a/test/test_skim.py +++ b/test/test_skim.py @@ -705,6 +705,86 @@ def test_ansi_and_read0(self): output = ":".join("{:02x}".format(ord(c)) for c in self.readonce()) self.assertTrue(output.find("61:00:62:0a") >= 0) + def test_smart_case_fuzzy(self): + """should behave correctly on case, #219""" + + # smart case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('abc')) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key('aBc')) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key('ABc')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('Enter')) + self.assertEqual('', self.readonce().strip()) + + def test_smart_case_exact(self): + """should behave correctly on case, #219""" + + # smart case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key("'abc")) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key("'aBc")) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key("'ABc")) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('Enter')) + self.assertEqual('', self.readonce().strip()) + + def test_ignore_case_fuzzy(self): + """should behave correctly on case, #219""" + + # ignore case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('--case ignore')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('abc')) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key('aBc')) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key('ABc')) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Key('Enter')) + + def test_ignore_case_exact(self): + """should behave correctly on case, #219""" + + # ignore case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('--case ignore')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key("'abc")) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key("'aBc")) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Ctrl('u'), Key("'ABc")) + self.tmux.until(lambda lines: lines[-3].startswith('> aBcXyZ')) + self.tmux.send_keys(Key('Enter')) + + def test_respect_case_fuzzy(self): + """should behave correctly on case, #219""" + + # respect case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('--case respect')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('abc')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('Enter')) + self.assertEqual('', self.readonce().strip()) + + def test_respect_case_exact(self): + """should behave correctly on case, #219""" + + # respect case + self.tmux.send_keys(f"echo -e 'aBcXyZ' | {self.sk('--case respect')}", Key('Enter')) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key("'abc")) + self.tmux.until(lambda lines: lines.ready_with_lines(1)) + self.tmux.send_keys(Key('Enter')) + self.assertEqual('', self.readonce().strip()) + def find_prompt(lines, interactive=False, reverse=False): linen = -1 prompt = ">"