use regex::Regex;
use crate::types::{Cursor, Query, Search};
#[derive(Debug, Clone, Default)]
pub struct SearchState {
pub pattern: Option<Regex>,
pub forward: bool,
pub matches: Vec<Vec<(usize, usize)>>,
pub generations: Vec<u64>,
pub wrap_around: bool,
}
impl SearchState {
pub fn new() -> Self {
Self {
pattern: None,
forward: true,
matches: Vec::new(),
generations: Vec::new(),
wrap_around: true,
}
}
pub fn set_pattern(&mut self, re: Option<Regex>) {
self.pattern = re;
self.matches.clear();
self.generations.clear();
}
pub fn matches_for(&mut self, row: usize, line: &str, dirty_gen: u64) -> &[(usize, usize)] {
let Some(ref re) = self.pattern else {
return &[];
};
if self.matches.len() <= row {
self.matches.resize_with(row + 1, Vec::new);
self.generations.resize(row + 1, u64::MAX);
}
if self.generations[row] != dirty_gen {
self.matches[row] = re.find_iter(line).map(|m| (m.start(), m.end())).collect();
self.generations[row] = dirty_gen;
}
&self.matches[row]
}
}
pub fn search_forward<B: Cursor + Query + Search>(
buf: &mut B,
state: &mut SearchState,
skip_current: bool,
) -> bool {
let Some(re) = state.pattern.clone() else {
return false;
};
let cursor = buf.cursor();
let total = buf.line_count();
if total == 0 {
return false;
}
let from = if skip_current {
let from_byte = buf.byte_offset(cursor);
buf.pos_at_byte(from_byte.saturating_add(1))
} else {
cursor
};
if let Some(range) = buf.find_next(from, &re) {
if !state.wrap_around && range.start.line < cursor.line {
return false;
}
Cursor::set_cursor(buf, range.start);
return true;
}
false
}
pub fn search_backward<B: Cursor + Query + Search>(
buf: &mut B,
state: &mut SearchState,
skip_current: bool,
) -> bool {
let Some(re) = state.pattern.clone() else {
return false;
};
let cursor = buf.cursor();
let total = buf.line_count();
if total == 0 {
return false;
}
let initial = buf.find_prev(cursor, &re);
let range = if skip_current {
match initial {
Some(m) if m.start == cursor => {
let cb = buf.byte_offset(m.start);
if cb == 0 {
None
} else {
let anchor = buf.pos_at_byte(cb.saturating_sub(1));
buf.find_prev(anchor, &re)
}
}
other => other,
}
} else {
initial
};
if let Some(range) = range {
if !state.wrap_around && range.start.line > cursor.line {
return false;
}
Cursor::set_cursor(buf, range.start);
return true;
}
false
}
pub fn search_matches<B: Query>(
buf: &B,
state: &mut SearchState,
dirty_gen: u64,
row: usize,
) -> Vec<(usize, usize)> {
if state.pattern.is_none() {
return Vec::new();
}
let line_count = buf.line_count() as usize;
if row >= line_count {
return Vec::new();
}
let line = buf.line(row as u32);
state.matches_for(row, line, dirty_gen).to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Pos;
use hjkl_buffer::Buffer;
fn re(pat: &str) -> Regex {
Regex::new(pat).unwrap()
}
#[test]
fn empty_state_no_match() {
let mut b = Buffer::from_str("anything");
let mut s = SearchState::new();
assert!(!search_forward(&mut b, &mut s, false));
assert!(!search_backward(&mut b, &mut s, false));
}
#[test]
fn forward_finds_first_match() {
let mut b = Buffer::from_str("foo bar foo baz");
let mut s = SearchState::new();
s.set_pattern(Some(re("foo")));
assert!(search_forward(&mut b, &mut s, false));
assert_eq!(Cursor::cursor(&b), Pos::new(0, 0));
}
#[test]
fn forward_skip_current_walks_past() {
let mut b = Buffer::from_str("foo bar foo baz");
let mut s = SearchState::new();
s.set_pattern(Some(re("foo")));
search_forward(&mut b, &mut s, false);
search_forward(&mut b, &mut s, true);
assert_eq!(Cursor::cursor(&b), Pos::new(0, 8));
}
#[test]
fn forward_wraps_to_top() {
let mut b = Buffer::from_str("zzz\nfoo");
Cursor::set_cursor(&mut b, Pos::new(1, 2));
let mut s = SearchState::new();
s.set_pattern(Some(re("zzz")));
s.wrap_around = true;
assert!(search_forward(&mut b, &mut s, true));
assert_eq!(Cursor::cursor(&b), Pos::new(0, 0));
}
#[test]
fn search_matches_caches_against_dirty_gen() {
let b = Buffer::from_str("foo bar");
let mut s = SearchState::new();
s.set_pattern(Some(re("bar")));
let dgen = b.dirty_gen();
let initial = search_matches(&b, &mut s, dgen, 0);
assert_eq!(initial, vec![(4, 7)]);
}
}