use ratatui::{
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
};
use crate::{app::Data, label::Handler as LabelHandler, screen::Handler as ScreenHandler};
use super::{KeyHandler, PopupOutput, Window, adjust_offset};
#[derive(PartialEq, Eq)]
pub(crate) struct Search {
pub(crate) input: String,
}
impl Search {
pub(crate) fn new() -> Self {
Self { input: String::new() }
}
}
impl KeyHandler for Search {
fn is_focusing(&self, window_type: super::Window) -> bool {
window_type == Window::Search
}
fn char(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler, c: char) {
self.input.push(c);
}
fn get_user_input(&self) -> PopupOutput<'_> {
PopupOutput::Str(&self.input)
}
fn backspace(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {
self.input.pop();
}
fn enter(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) {
let byte_sequence_to_search = self.input.as_bytes();
if byte_sequence_to_search.is_empty() {
labels.notification = "Empty search query".into();
return;
}
app.search_term.clone_from(&self.input);
app.reindex_search();
perform_search(app, display, labels, &SearchDirection::Forward);
}
fn dimensions(&self) -> Option<(u16, u16)> {
Some((50, 3))
}
fn widget(&self) -> Paragraph<'_> {
Paragraph::new(Span::styled(&self.input, Style::default().fg(Color::White))).block(
Block::default()
.title("Search:")
.borders(Borders::ALL)
.style(Style::default().fg(Color::Yellow)),
)
}
}
pub(crate) enum SearchDirection {
Forward,
Backward,
}
pub(crate) fn perform_search(
app: &mut Data,
display: &mut ScreenHandler,
labels: &mut LabelHandler,
search_direction: &SearchDirection,
) {
if app.search_term.is_empty() {
return;
}
if app.dirty {
app.reindex_search();
}
if app.search_offsets.is_empty() {
labels.notification = "Query not found".into();
return;
}
let idx = get_next_match_index(&app.search_offsets, app.offset, search_direction);
let found_position = *app.search_offsets.get(idx).expect("There should be at least one result");
labels.notification =
format!("Search: {} [{}/{}]", app.search_term, idx + 1, app.search_offsets.len());
app.offset = found_position;
labels.update_all(&app.contents[app.offset..]);
adjust_offset(app, display, labels);
}
fn get_next_match_index(
search_offsets: &[usize],
current_offset: usize,
search_direction: &SearchDirection,
) -> usize {
match search_direction {
SearchDirection::Forward => search_offsets
.binary_search(&(current_offset + 1))
.unwrap_or_else(|i| if i >= search_offsets.len() { 0 } else { i }),
SearchDirection::Backward => search_offsets
.binary_search(&(current_offset.checked_sub(1).unwrap_or(usize::MAX)))
.unwrap_or_else(|i| if i == 0 { search_offsets.len() - 1 } else { i - 1 }),
}
}
#[cfg(test)]
mod tests {
use super::{SearchDirection, get_next_match_index};
#[test]
fn test_search() {
fn search(
search_offsets: &[usize],
current_offset: usize,
search_direction: &SearchDirection,
) -> usize {
let idx = get_next_match_index(search_offsets, current_offset, search_direction);
search_offsets[idx]
}
let search_offsets = vec![1, 4, 5, 7];
assert_eq!(search(&search_offsets, 2, &SearchDirection::Backward), 1);
assert_eq!(search(&search_offsets, 3, &SearchDirection::Backward), 1);
assert_eq!(search(&search_offsets, 4, &SearchDirection::Backward), 1);
assert_eq!(search(&search_offsets, 4, &SearchDirection::Forward), 5);
assert_eq!(search(&search_offsets, 7, &SearchDirection::Forward), 1);
assert_eq!(search(&search_offsets, 1, &SearchDirection::Backward), 7);
assert_eq!(search(&search_offsets, 0, &SearchDirection::Backward), 7);
let search_offsets = vec![3];
assert_eq!(search(&search_offsets, 4, &SearchDirection::Backward), 3);
assert_eq!(search(&search_offsets, 3, &SearchDirection::Backward), 3);
assert_eq!(search(&search_offsets, 2, &SearchDirection::Backward), 3);
}
}