use memchr::memmem;
use regex::bytes::{Regex, RegexBuilder};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchKind {
Literal,
Regex,
}
impl SearchKind {
pub fn label(self) -> &'static str {
match self {
SearchKind::Literal => "literal",
SearchKind::Regex => "regex",
}
}
pub fn toggled(self) -> Self {
match self {
SearchKind::Literal => SearchKind::Regex,
SearchKind::Regex => SearchKind::Literal,
}
}
}
#[derive(Debug, Clone)]
pub struct Search {
query: String,
kind: SearchKind,
matcher: Matcher,
}
#[derive(Debug, Clone)]
enum Matcher {
Literal(Box<memmem::Finder<'static>>),
Regex(Regex),
}
fn has_uppercase(s: &str) -> bool {
s.chars().any(|c| c.is_uppercase())
}
impl Search {
pub fn new(query: &str, kind: SearchKind) -> Result<Self, String> {
let case_insensitive = !has_uppercase(query);
let matcher = match kind {
SearchKind::Literal if !case_insensitive => {
Matcher::Literal(Box::new(memmem::Finder::new(query.as_bytes()).into_owned()))
}
SearchKind::Literal => {
build_regex(®ex::escape(query), true)?
}
SearchKind::Regex => build_regex(query, case_insensitive)?,
};
Ok(Search {
query: query.to_string(),
kind,
matcher,
})
}
pub fn query(&self) -> &str {
&self.query
}
pub fn kind(&self) -> SearchKind {
self.kind
}
pub fn line_matches(&self, line: &str) -> bool {
match &self.matcher {
Matcher::Literal(f) => f.find(line.as_bytes()).is_some(),
Matcher::Regex(re) => re.is_match(line.as_bytes()),
}
}
pub fn match_ranges(&self, line: &str) -> Vec<(usize, usize)> {
let bytes = line.as_bytes();
match &self.matcher {
Matcher::Literal(f) => {
let len = self.query.len();
f.find_iter(bytes)
.map(|start| (start, start + len))
.collect()
}
Matcher::Regex(re) => re
.find_iter(bytes)
.filter(|m| m.end() > m.start())
.map(|m| (m.start(), m.end()))
.collect(),
}
}
}
fn build_regex(pattern: &str, case_insensitive: bool) -> Result<Matcher, String> {
RegexBuilder::new(pattern)
.case_insensitive(case_insensitive)
.build()
.map(Matcher::Regex)
.map_err(|e| format!("invalid regex: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_literal_smart_case() {
let s = Search::new("error", SearchKind::Literal).unwrap();
assert!(s.line_matches("an ERROR occurred"));
assert!(s.line_matches("error here"));
let s = Search::new("Error", SearchKind::Literal).unwrap();
assert!(s.line_matches("an Error"));
assert!(!s.line_matches("an error"));
}
#[test]
fn test_literal_match_ranges() {
let s = Search::new("ab", SearchKind::Literal).unwrap();
assert_eq!(s.match_ranges("xabyab"), vec![(1, 3), (4, 6)]);
}
#[test]
fn test_regex_match_and_ranges() {
let s = Search::new(r"\d{3}", SearchKind::Regex).unwrap();
assert!(s.line_matches("code 404 here"));
assert_eq!(s.match_ranges("a123b456"), vec![(1, 4), (5, 8)]);
}
#[test]
fn test_regex_smart_case() {
let s = Search::new("warn", SearchKind::Regex).unwrap();
assert!(s.line_matches("WARN: x")); let s = Search::new("WARN", SearchKind::Regex).unwrap();
assert!(!s.line_matches("warn: x")); }
#[test]
fn test_invalid_regex_is_error_not_panic() {
let err = Search::new("(unclosed", SearchKind::Regex);
assert!(err.is_err());
}
#[test]
fn test_zero_width_regex_not_infinite() {
let s = Search::new("x*", SearchKind::Regex).unwrap();
assert!(s.match_ranges("zzz").is_empty());
}
}