#[cfg(feature = "find-replace")]
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FindOptions {
pub case_sensitive: bool,
pub whole_word: bool,
pub use_regex: bool,
pub wrap_around: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FindResult {
pub start: usize,
pub end: usize,
}
impl FindResult {
#[must_use]
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
#[must_use]
pub const fn len(&self) -> usize {
self.end - self.start
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone, Default)]
pub struct FindState {
pub query: String,
pub replacement: String,
pub options: FindOptions,
pub matches: Vec<FindResult>,
pub current_index: usize,
pub is_visible: bool,
pub is_replace_mode: bool,
}
impl FindState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn search(&mut self, text: &str) {
self.matches.clear();
self.current_index = 0;
if self.query.is_empty() {
return;
}
if self.options.use_regex {
self.search_regex(text);
} else {
self.search_literal(text);
}
}
fn search_literal(&mut self, text: &str) {
let search_text = if self.options.case_sensitive {
text.to_string()
} else {
text.to_lowercase()
};
let query = if self.options.case_sensitive {
self.query.clone()
} else {
self.query.to_lowercase()
};
let mut start = 0;
while let Some(pos) = search_text[start..].find(&query) {
let match_start = start + pos;
let match_end = match_start + self.query.len();
if self.options.whole_word {
let is_start_boundary = match_start == 0
|| !text[..match_start]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);
let is_end_boundary = match_end >= text.len()
|| !text[match_end..]
.chars()
.next()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);
if !is_start_boundary || !is_end_boundary {
start = match_start + 1;
continue;
}
}
self.matches.push(FindResult::new(match_start, match_end));
start = match_end;
}
}
#[cfg(feature = "find-replace")]
fn search_regex(&mut self, text: &str) {
let pattern = if self.options.case_sensitive {
self.query.clone()
} else {
format!("(?i){}", self.query)
};
let pattern = if self.options.whole_word {
format!(r"\b{}\b", pattern)
} else {
pattern
};
if let Ok(re) = Regex::new(&pattern) {
for m in re.find_iter(text) {
self.matches.push(FindResult::new(m.start(), m.end()));
}
}
}
pub fn next(&mut self) -> Option<FindResult> {
if self.matches.is_empty() {
return None;
}
self.current_index = (self.current_index + 1) % self.matches.len();
self.current_match()
}
pub fn prev(&mut self) -> Option<FindResult> {
if self.matches.is_empty() {
return None;
}
self.current_index = if self.current_index == 0 {
self.matches.len() - 1
} else {
self.current_index - 1
};
self.current_match()
}
#[must_use]
pub fn current_match(&self) -> Option<FindResult> {
self.matches.get(self.current_index).copied()
}
#[must_use]
pub fn match_count(&self) -> usize {
self.matches.len()
}
#[must_use]
pub fn has_matches(&self) -> bool {
!self.matches.is_empty()
}
pub fn replace_current(&self, text: &str) -> Option<String> {
let current = self.current_match()?;
let mut result = String::with_capacity(text.len());
result.push_str(&text[..current.start]);
result.push_str(&self.replacement);
result.push_str(&text[current.end..]);
Some(result)
}
pub fn replace_all(&self, text: &str) -> String {
if self.matches.is_empty() {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
let mut last_end = 0;
for m in &self.matches {
result.push_str(&text[last_end..m.start]);
result.push_str(&self.replacement);
last_end = m.end;
}
result.push_str(&text[last_end..]);
result
}
pub fn show(&mut self) {
self.is_visible = true;
self.is_replace_mode = false;
}
pub fn show_replace(&mut self) {
self.is_visible = true;
self.is_replace_mode = true;
}
pub fn hide(&mut self) {
self.is_visible = false;
}
pub fn clear(&mut self) {
self.query.clear();
self.matches.clear();
self.current_index = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_literal() {
let mut state = FindState::new();
state.query = "hello".to_string();
state.search("hello world hello");
assert_eq!(state.match_count(), 2);
assert_eq!(state.matches[0], FindResult::new(0, 5));
assert_eq!(state.matches[1], FindResult::new(12, 17));
}
#[test]
fn test_find_case_insensitive() {
let mut state = FindState::new();
state.query = "Hello".to_string();
state.options.case_sensitive = false;
state.search("hello HELLO Hello");
assert_eq!(state.match_count(), 3);
}
#[test]
fn test_find_whole_word() {
let mut state = FindState::new();
state.query = "test".to_string();
state.options.whole_word = true;
state.search("test testing tested test");
assert_eq!(state.match_count(), 2);
}
#[test]
fn test_replace_all() {
let mut state = FindState::new();
state.query = "old".to_string();
state.replacement = "new".to_string();
state.search("old and old");
let result = state.replace_all("old and old");
assert_eq!(result, "new and new");
}
}