use regex::Regex;
use crate::types::{Cursor, Query, Search};
pub fn vim_to_rust_regex(pat: &str) -> String {
let mut out = String::with_capacity(pat.len());
let mut chars = pat.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.peek() {
Some('<') => {
chars.next();
out.push_str(r"\b");
}
Some('>') => {
chars.next();
out.push_str(r"\b");
}
_ => {
out.push('\\');
if let Some(next) = chars.next() {
out.push(next);
}
}
}
} else {
out.push(ch);
}
}
out
}
#[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()
}
fn vim_re(pat: &str) -> Regex {
Regex::new(&vim_to_rust_regex(pat)).unwrap()
}
#[test]
fn vim_boundary_rewrites_to_b() {
assert_eq!(vim_to_rust_regex(r"\<foo\>"), r"\bfoo\b");
assert_eq!(vim_to_rust_regex(r"\<"), r"\b");
assert_eq!(vim_to_rust_regex(r"\>"), r"\b");
}
#[test]
fn escaped_backslash_left_alone() {
let input = r"\\<";
let output = vim_to_rust_regex(input);
assert_eq!(output, r"\\<");
}
#[test]
fn other_escapes_unchanged() {
assert_eq!(vim_to_rust_regex(r"\b"), r"\b");
assert_eq!(vim_to_rust_regex(r"\B"), r"\B");
assert_eq!(vim_to_rust_regex(r"\d+"), r"\d+");
assert_eq!(vim_to_rust_regex(r"^\w+$"), r"^\w+$");
}
#[test]
fn mixed_boundary_and_word_class() {
assert_eq!(vim_to_rust_regex(r"\<\w+\>"), r"\b\w+\b");
}
#[test]
fn vim_boundary_matches_standalone_word_not_suffix() {
let re = vim_re(r"foo\<bar\>");
assert!(!re.is_match("foobar"));
let re2 = vim_re(r"\<bar\>");
assert!(re2.is_match("foo bar baz"));
assert!(!re2.is_match("foobar"));
}
#[test]
fn vim_boundary_start_only() {
let re = vim_re(r"\<word");
assert!(re.is_match("word here"));
assert!(re.is_match("some word here"));
assert!(!re.is_match("sword"));
assert!(!re.is_match("aword"));
}
#[test]
fn vim_boundary_end_only() {
let re = vim_re(r"word\>");
assert!(re.is_match("some word"));
assert!(re.is_match("word"));
assert!(!re.is_match("words"));
assert!(!re.is_match("wordsmith"));
}
#[test]
fn existing_b_boundary_unchanged() {
let re = vim_re(r"\bfoo\b");
assert!(re.is_match("foo"));
assert!(re.is_match("a foo b"));
assert!(!re.is_match("foobar"));
assert!(!re.is_match("afoo"));
}
#[test]
fn vim_whole_word_pattern() {
let re = vim_re(r"\<\w+\>");
let matches: Vec<_> = re.find_iter("foo bar baz").map(|m| m.as_str()).collect();
assert_eq!(matches, vec!["foo", "bar", "baz"]);
}
#[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)]);
}
}