Skip to content

Commit

Permalink
fzf: make Pattern::parse behave like fzf
Browse files Browse the repository at this point in the history
  • Loading branch information
noib3 committed Dec 1, 2023
1 parent 7ab30c6 commit a1eaa04
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 128 deletions.
6 changes: 4 additions & 2 deletions src/algos/fzf/fzf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ mod tests {
#[test]
fn equal_match_1() {
let pattern =
Pattern::parse("^AbC$".chars().collect::<Vec<_>>().leak());
Pattern::parse("^AbC$".chars().collect::<Vec<_>>().leak())
.unwrap();

let mut ranges_buf = Vec::new();

Expand Down Expand Up @@ -675,7 +676,8 @@ mod tests {

#[test]
fn exact_match_1() {
let pattern = Pattern::parse("abc".chars().collect::<Vec<_>>().leak());
let pattern =
Pattern::parse("abc".chars().collect::<Vec<_>>().leak()).unwrap();

let mut ranges_buf = Vec::new();

Expand Down
5 changes: 0 additions & 5 deletions src/algos/fzf/fzf_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,6 @@ impl Fzf for FzfV1 {
candidate: Candidate,
ranges: &mut MatchedRanges,
) -> Option<Score> {
// TODO: can we remove this?
if pattern.is_empty() {
return Some(0);
}

let is_sensitive = match self.case_sensitivity {
CaseSensitivity::Sensitive => true,
CaseSensitivity::Insensitive => false,
Expand Down
5 changes: 0 additions & 5 deletions src/algos/fzf/fzf_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,6 @@ impl Fzf for FzfV2 {
candidate: Candidate,
ranges: &mut MatchedRanges,
) -> Option<Score> {
// TODO: can we remove this?
if pattern.is_empty() {
return Some(0);
}

let is_sensitive = match self.case_sensitivity {
CaseSensitivity::Sensitive => true,
CaseSensitivity::Insensitive => false,
Expand Down
4 changes: 2 additions & 2 deletions src/algos/fzf/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ impl<'buf, 's> Iterator for Patterns<'buf, 's> {
let word_is_condition = word != OR_BLOCK_SEPARATOR;

if word_is_condition {
let word = Pattern::parse(word);
let Some(word) = Pattern::parse(word) else { continue };

if looking_for_or {
self.next = Some(word);
Expand Down Expand Up @@ -506,7 +506,7 @@ mod patterns_tests {
}

fn pattern(s: &str) -> Pattern<'static> {
Pattern::parse(s.chars().collect::<Vec<_>>().leak())
Pattern::parse(s.chars().collect::<Vec<_>>().leak()).unwrap()
}

#[test]
Expand Down
194 changes: 82 additions & 112 deletions src/algos/fzf/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,94 +219,60 @@ impl<'a> Pattern<'a> {

/// TODO: docs
#[inline]
pub(super) fn parse(mut text: &'a [char]) -> Self {
pub(super) fn parse(mut text: &'a [char]) -> Option<Self> {
debug_assert!(!text.is_empty());

let first_char = text[0];

// If the pattern is a single character we always parse it as a fuzzy
// match. This diverges from fzf which seems to do the same for a
// single '$', but not for a single '^', '!', or '''.
if text.len() == 1 {
return Self {
text,
has_uppercase: first_char.is_uppercase(),
match_type: MatchType::Fuzzy,
is_inverse: false,
leading_spaces: 0,
trailing_spaces: 0,
};
}

let last_char = text[text.len() - 1];

let mut is_inverse = false;

let match_type;
let mut match_type = MatchType::Fuzzy;

match first_char {
'\'' => {
text = &text[1..];
match_type = MatchType::Exact;
},

'^' if last_char == '$' => {
text = &text[1..text.len() - 1];
match_type = MatchType::EqualExact;
},

'^' => {
text = &text[1..];
match_type = MatchType::PrefixExact;
},

'!' if text.get(1).copied() == Some('\'') => {
text = &text[2..];
match_type = MatchType::Fuzzy;
is_inverse = true;
},
if starts_with(text, '!') {
is_inverse = true;
match_type = MatchType::Exact;
text = &text[1..];
}

'!' if text.get(1).copied() == Some('^') => {
text = &text[2..];
match_type = MatchType::PrefixExact;
is_inverse = true;
},
if ends_with(text, '$') && text.len() > 1 {
match_type = MatchType::SuffixExact;
text = &text[..text.len() - 1];
}

'!' if last_char == '$' => {
text = &text[1..text.len() - 1];
match_type = MatchType::SuffixExact;
is_inverse = true;
},
if starts_with(text, '\'') {
match_type =
if !is_inverse { MatchType::Exact } else { MatchType::Fuzzy };

'!' => {
text = &text[1..];
match_type = MatchType::Exact;
is_inverse = true;
},
text = &text[1..];
} else if starts_with(text, '^') {
match_type = if match_type == MatchType::SuffixExact {
MatchType::EqualExact
} else {
MatchType::PrefixExact
};

_ if last_char == '$' => {
text = &text[..text.len() - 1];
match_type = MatchType::SuffixExact;
},
text = &text[1..];
}

_ => {
match_type = MatchType::Fuzzy;
},
if text.is_empty() {
return None;
}

let has_uppercase = text.iter().copied().any(char::is_uppercase);

let leading_spaces = text.iter().take_while(|&&c| c == ' ').count();

let trailing_spaces =
text.iter().rev().take_while(|&&c| c == ' ').count();

Self {
let this = Self {
is_inverse,
match_type,
text,
has_uppercase,
leading_spaces,
trailing_spaces,
has_uppercase: text.iter().copied().any(char::is_uppercase),
text,
match_type,
is_inverse,
}
};

Some(this)
}

/// TODO: docs
Expand All @@ -316,9 +282,18 @@ impl<'a> Pattern<'a> {
}
}

#[inline(always)]
fn ends_with(haystack: &[char], needle: char) -> bool {
haystack.last().copied() == Some(needle)
}

#[inline(always)]
fn starts_with(haystack: &[char], needle: char) -> bool {
haystack.first().copied() == Some(needle)
}

/// TODO: docs
#[derive(Default, Clone, Copy)]
#[cfg_attr(test, derive(Debug, PartialEq))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum MatchType {
/// TODO: docs
#[default]
Expand All @@ -341,65 +316,60 @@ pub(super) enum MatchType {
mod tests {
use super::*;

fn pattern(s: &str) -> Pattern<'static> {
Pattern::parse(s.chars().collect::<Vec<_>>().leak())
}

#[test]
fn pattern_parse_single_apostrophe() {
let pattern = pattern("'");
assert_eq!(pattern.into_string(), "'");
fn pattern_parse_specials_1() {
assert!(Pattern::parse(&['\'']).is_none());
assert!(Pattern::parse(&['^']).is_none());
assert!(Pattern::parse(&['!']).is_none());

let pattern = Pattern::parse(&['$']).unwrap();
assert_eq!(pattern.into_string(), "$");
assert_eq!(pattern.match_type, MatchType::Fuzzy);
}

#[test]
fn pattern_parse_single_caret() {
let pattern = pattern("^");
fn pattern_parse_specials_2() {
assert!(Pattern::parse(&['!', '\'']).is_none());
assert!(Pattern::parse(&['!', '^']).is_none());
assert!(Pattern::parse(&['\'', '$']).is_none());
assert!(Pattern::parse(&['^', '$']).is_none());

let pattern = Pattern::parse(&['\'', '^']).unwrap();
assert_eq!(pattern.into_string(), "^");
assert_eq!(pattern.match_type, MatchType::Fuzzy);
}
assert_eq!(pattern.match_type, MatchType::Exact);

#[test]
fn pattern_parse_single_dollar() {
let pattern = pattern("$");
let pattern = Pattern::parse(&['!', '$']).unwrap();
assert_eq!(pattern.into_string(), "$");
assert_eq!(pattern.match_type, MatchType::Fuzzy);
}
assert_eq!(pattern.match_type, MatchType::Exact);
assert!(pattern.is_inverse);

#[test]
fn pattern_parse_single_exclamation() {
let pattern = pattern("!");
let pattern = Pattern::parse(&['!', '!']).unwrap();
assert_eq!(pattern.into_string(), "!");
assert_eq!(pattern.match_type, MatchType::Fuzzy);
}

#[test]
fn pattern_parse_double_caret() {
let pattern = pattern("^^");
assert_eq!(pattern.into_string(), "^");
assert_eq!(pattern.match_type, MatchType::PrefixExact);
}
assert_eq!(pattern.match_type, MatchType::Exact);
assert!(pattern.is_inverse);

#[test]
fn pattern_parse_double_dollar() {
let pattern = pattern("$$");
let pattern = Pattern::parse(&['$', '$']).unwrap();
assert_eq!(pattern.into_string(), "$");
assert_eq!(pattern.match_type, MatchType::SuffixExact);
}

#[test]
fn pattern_parse_exclamation_caret() {
let pattern = pattern("!^");
assert_eq!(pattern.into_string(), "");
assert_eq!(pattern.match_type, MatchType::PrefixExact);
assert!(pattern.is_inverse);
fn pattern_parse_specials_3() {
assert!(Pattern::parse(&['!', '^', '$']).is_none());

let pattern = Pattern::parse(&['\'', '^', '$']).unwrap();
assert_eq!(pattern.into_string(), "^");
assert_eq!(pattern.match_type, MatchType::Exact);

let pattern = Pattern::parse(&['\'', '!', '$']).unwrap();
assert_eq!(pattern.into_string(), "!");
assert_eq!(pattern.match_type, MatchType::Exact);
}

#[test]
fn pattern_parse_exlamation_dollar() {
let pattern = pattern("!$");
assert_eq!(pattern.into_string(), "");
assert_eq!(pattern.match_type, MatchType::SuffixExact);
assert!(pattern.is_inverse);
fn pattern_parse_specials_4() {
let pattern = Pattern::parse(&['\'', '^', '$', '$']).unwrap();
assert_eq!(pattern.into_string(), "^$");
assert_eq!(pattern.match_type, MatchType::Exact);
}
}
2 changes: 1 addition & 1 deletion tests/fzf_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ fn fzf_v1_score_5() {
)
.unwrap();

assert_eq!(ranges, [1..2, 7..9]);
assert_eq!(ranges, [7..9]);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion tests/fzf_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ fn fzf_v2_score_4() {
)
.unwrap();

assert_eq!(ranges, [1..2, 7..9]);
assert_eq!(ranges, [7..9]);
}

#[test]
Expand Down

0 comments on commit a1eaa04

Please sign in to comment.