lview/src/search.rs
2026-06-17 21:33:03 +02:00

111 lines
2.6 KiB
Rust

use regex::Regex;
#[derive(Debug, Clone)]
pub struct SearchState {
pub query: String,
pub is_regex: bool,
}
impl Default for SearchState {
fn default() -> Self {
Self {
query: String::new(),
is_regex: true,
}
}
}
impl SearchState {
pub fn compile(&self) -> Option<Regex> {
if self.query.is_empty() {
return None;
}
let pattern = if self.is_regex {
self.query.clone()
} else {
regex::escape(&self.query)
};
regex::RegexBuilder::new(&pattern)
.case_insensitive(true)
.build()
.ok()
}
pub fn matches(&self, line: &str) -> bool {
self.compile().map(|re| re.is_match(line)).unwrap_or(true)
}
pub fn next_match(&self, lines: &[String], from: usize) -> Option<usize> {
let re = self.compile()?;
let mut idx = from + 1;
if idx >= lines.len() {
idx = 0;
}
let start = idx;
loop {
if re.is_match(&lines[idx]) {
return Some(idx);
}
idx = (idx + 1) % lines.len();
if idx == start {
return None;
}
}
}
pub fn prev_match(&self, lines: &[String], from: usize) -> Option<usize> {
let re = self.compile()?;
let len = lines.len();
if len == 0 {
return None;
}
let mut idx = from.checked_sub(1).unwrap_or(len - 1);
let start = idx;
loop {
if re.is_match(&lines[idx]) {
return Some(idx);
}
if idx == 0 {
idx = len - 1;
} else {
idx -= 1;
}
if idx == start {
return None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_matches() {
let s = SearchState {
query: "err".into(),
..Default::default()
};
assert!(s.matches("ERROR: something failed"));
assert!(!s.matches("INFO: ok"));
}
#[test]
fn test_next_prev() {
let lines = vec![
"INFO ok".into(),
"ERROR one".into(),
"INFO fine".into(),
"ERROR two".into(),
];
let s = SearchState {
query: "ERROR".into(),
..Default::default()
};
assert_eq!(s.next_match(&lines, 0), Some(1));
assert_eq!(s.next_match(&lines, 1), Some(3));
assert_eq!(s.next_match(&lines, 3), Some(1));
assert_eq!(s.prev_match(&lines, 0), Some(3));
}
}