use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchQuery {
pub needle: String,
pub case_sensitive: bool,
}
impl SearchQuery {
pub fn new(needle: impl Into<String>, case_sensitive: bool) -> Self {
Self {
needle: needle.into(),
case_sensitive,
}
}
}
fn grapheme_eq(a: &str, b: &str, case_sensitive: bool) -> bool {
if case_sensitive {
return a == b;
}
a.chars()
.flat_map(char::to_lowercase)
.eq(b.chars().flat_map(char::to_lowercase))
}
pub fn find_in_line(line: &str, query: &SearchQuery) -> Vec<(usize, usize)> {
if query.needle.is_empty() {
return Vec::new();
}
let needle: Vec<&str> = query.needle.graphemes(true).collect();
find_in_line_with(line, &needle, query)
}
pub(crate) fn find_in_line_with(
line: &str,
needle: &[&str],
query: &SearchQuery,
) -> Vec<(usize, usize)> {
if needle.is_empty() {
return Vec::new();
}
if query.case_sensitive && !line.contains(query.needle.as_str()) {
return Vec::new();
}
let haystack: Vec<&str> = line.graphemes(true).collect();
if needle.len() > haystack.len() {
return Vec::new();
}
let mut hits = Vec::new();
let mut i = 0usize;
while i + needle.len() <= haystack.len() {
let matched = haystack[i..i + needle.len()]
.iter()
.zip(needle)
.all(|(h, n)| grapheme_eq(h, n, query.case_sensitive));
if matched {
hits.push((i, i + needle.len()));
i += needle.len();
} else {
i += 1;
}
}
hits
}