use core::fmt::Write;
#[derive(Clone, Copy)]
pub struct FzfQuery<'a> {
pub(super) search_mode: SearchMode<'a>,
}
#[derive(Clone, Copy)]
pub(super) enum SearchMode<'a> {
Extended(&'a [Condition<'a>]),
NotExtended(Pattern<'a>),
}
impl core::fmt::Debug for FzfQuery<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self.search_mode {
SearchMode::Extended(conditions) => conditions
.iter()
.map(|condition| format!("{:?}", condition))
.collect::<Vec<_>>()
.join(" && "),
SearchMode::NotExtended(pattern) => pattern.into_string(),
};
f.debug_tuple("FzfQuery").field(&s).finish()
}
}
impl<'a> FzfQuery<'a> {
#[inline]
pub(super) fn is_empty(&self) -> bool {
match self.search_mode {
SearchMode::Extended(conditions) => conditions.is_empty(),
SearchMode::NotExtended(pattern) => pattern.is_empty(),
}
}
#[inline]
pub(super) fn new_extended(conditions: &'a [Condition<'a>]) -> Self {
if conditions.len() == 1 {
let mut patterns = conditions[0].iter();
let first_pattern = patterns
.next()
.expect("conditions always have at least one pattern");
if patterns.next().is_none()
&& matches!(first_pattern.match_type, MatchType::Fuzzy)
{
return Self {
search_mode: SearchMode::NotExtended(first_pattern),
};
}
}
Self { search_mode: SearchMode::Extended(conditions) }
}
#[inline]
pub(super) fn new_not_extended(chars: &'a [char]) -> Self {
Self { search_mode: SearchMode::NotExtended(Pattern::raw(chars)) }
}
}
#[derive(Default, Clone, Copy)]
pub(super) struct Condition<'a> {
pub(super) or_patterns: &'a [Pattern<'a>],
}
impl core::fmt::Debug for Condition<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.or_patterns {
[] => Ok(()),
[pattern] => pattern.into_string().fmt(f),
_ => {
f.write_char('(')?;
let len = self.or_patterns.len();
for (idx, pattern) in self.iter().enumerate() {
let is_last = idx + 1 == len;
pattern.into_string().fmt(f)?;
if !is_last {
f.write_str(" || ")?;
}
}
f.write_char(')')
},
}
}
}
impl<'a> Condition<'a> {
#[cfg(test)]
pub(super) fn or_patterns(&self) -> &'a [Pattern<'a>] {
self.or_patterns
}
#[inline]
pub(super) fn iter(
&self,
) -> impl Iterator<Item = Pattern<'a>> + ExactSizeIterator + '_ {
self.or_patterns.iter().copied()
}
#[inline]
pub(super) fn new(or_patterns: &'a [Pattern<'a>]) -> Self {
Self { or_patterns }
}
}
#[derive(Default, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq))]
pub(super) struct Pattern<'a> {
text: &'a [char],
pub(super) has_uppercase: bool,
pub(super) match_type: MatchType,
pub(super) is_inverse: bool,
pub(super) leading_spaces: usize,
pub(super) trailing_spaces: usize,
}
impl core::fmt::Debug for Pattern<'_> {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.into_string().fmt(f)
}
}
impl<'a> Pattern<'a> {
#[inline(always)]
pub(super) fn char_len(&self) -> usize {
self.text.len()
}
#[inline(always)]
pub(super) fn char(&self, idx: usize) -> char {
self.text[idx]
}
#[inline]
pub(crate) fn chars(
&self,
) -> impl Iterator<Item = char> + DoubleEndedIterator + '_ {
self.text.iter().copied()
}
#[inline]
pub(super) fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[inline]
pub(super) fn into_string(self) -> String {
self.text.iter().collect::<String>()
}
#[inline(always)]
pub(super) fn leading_spaces(&self) -> usize {
self.leading_spaces
}
#[inline]
fn raw(text: &'a [char]) -> Self {
let leading_spaces = text.iter().take_while(|&&c| c == ' ').count();
let trailing_spaces =
text.iter().rev().take_while(|&&c| c == ' ').count();
Self {
leading_spaces,
trailing_spaces,
has_uppercase: text.iter().copied().any(char::is_uppercase),
text,
match_type: MatchType::Fuzzy,
is_inverse: false,
}
}
#[inline]
pub(super) fn parse(mut text: &'a [char]) -> Option<Self> {
debug_assert!(!text.is_empty());
let mut is_inverse = false;
let mut match_type = MatchType::Fuzzy;
if starts_with(text, '!') {
is_inverse = true;
match_type = MatchType::Exact;
text = &text[1..];
}
if ends_with(text, '$') && text.len() > 1 {
match_type = MatchType::SuffixExact;
text = &text[..text.len() - 1];
}
if starts_with(text, '\'') {
match_type =
if !is_inverse { MatchType::Exact } else { MatchType::Fuzzy };
text = &text[1..];
} else if starts_with(text, '^') {
match_type = if match_type == MatchType::SuffixExact {
MatchType::EqualExact
} else {
MatchType::PrefixExact
};
text = &text[1..];
}
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();
let this = Self {
is_inverse,
match_type,
text,
has_uppercase,
leading_spaces,
trailing_spaces,
};
Some(this)
}
#[inline(always)]
pub(super) fn trailing_spaces(&self) -> usize {
self.trailing_spaces
}
}
#[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)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum MatchType {
#[default]
Fuzzy,
Exact,
PrefixExact,
SuffixExact,
EqualExact,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
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_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::Exact);
let pattern = Pattern::parse(&['!', '$']).unwrap();
assert_eq!(pattern.into_string(), "$");
assert_eq!(pattern.match_type, MatchType::Exact);
assert!(pattern.is_inverse);
let pattern = Pattern::parse(&['!', '!']).unwrap();
assert_eq!(pattern.into_string(), "!");
assert_eq!(pattern.match_type, MatchType::Exact);
assert!(pattern.is_inverse);
let pattern = Pattern::parse(&['$', '$']).unwrap();
assert_eq!(pattern.into_string(), "$");
assert_eq!(pattern.match_type, MatchType::SuffixExact);
}
#[test]
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_specials_4() {
let pattern = Pattern::parse(&['\'', '^', '$', '$']).unwrap();
assert_eq!(pattern.into_string(), "^$");
assert_eq!(pattern.match_type, MatchType::Exact);
}
}