mod line_match;
mod state;
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use parking_lot::{Mutex, RwLock};
pub(crate) use self::{line_match::LineMatch, state::State};
use crate::{
search::{Interrupter, SearchResult, Searchable, Status},
todo_file::{Action, TodoFile},
};
const LOCK_DURATION: Duration = Duration::from_millis(100);
#[derive(Clone, Debug)]
pub(crate) struct Search {
cursor: Arc<AtomicUsize>,
state: Arc<RwLock<State>>,
todo_file: Arc<Mutex<TodoFile>>,
}
impl Searchable for Search {
fn reset(&mut self) {
self.state.write().reset();
}
fn search(&mut self, interrupter: Interrupter, term: &str) -> SearchResult {
let Some(todo_file) = self.todo_file.try_lock_for(LOCK_DURATION)
else {
return SearchResult::None;
};
let Some(mut state) = self.state.try_write_for(LOCK_DURATION)
else {
return SearchResult::None;
};
if state.try_invalidate_search(todo_file.version(), term) {
self.cursor.store(0, Ordering::Release);
}
let mut has_matches = false;
let mut complete = false;
state.set_status(Status::Active);
let mut cursor = self.cursor.load(Ordering::Acquire);
while interrupter.should_continue() {
let Some(line) = todo_file.get_line(cursor)
else {
complete = true;
break;
};
let action = *line.get_action();
let has_hash_match = match action {
Action::Break | Action::Noop | Action::Label | Action::Reset | Action::Merge | Action::Exec => false,
Action::Drop
| Action::Edit
| Action::Fixup
| Action::Pick
| Action::Reword
| Action::Squash
| Action::UpdateRef => line.get_hash().starts_with(term),
};
let has_content_match = match action {
Action::Break | Action::Noop => false,
Action::Drop
| Action::Edit
| Action::Fixup
| Action::Pick
| Action::Reword
| Action::Squash
| Action::UpdateRef
| Action::Label
| Action::Reset
| Action::Merge
| Action::Exec => line.get_content().contains(term),
};
has_matches = state.push_match(LineMatch::new(cursor, has_hash_match, has_content_match)) || has_matches;
cursor += 1;
}
self.cursor.store(cursor, Ordering::Release);
if has_matches {
SearchResult::Updated
}
else if complete {
state.set_status(Status::Complete);
SearchResult::Complete
}
else {
SearchResult::None
}
}
}
impl Search {
/// Create a new instance
#[inline]
#[must_use]
pub(crate) fn new(todo_file: Arc<Mutex<TodoFile>>) -> Self {
Self {
cursor: Arc::new(AtomicUsize::new(0)),
state: Arc::new(RwLock::new(State::new())),
todo_file,
}
}
/// Select the next search result
#[inline]
#[allow(clippy::missing_panics_doc)]
pub(crate) fn next(&mut self) -> Option<usize> {
let mut state = self.state.write();
if state.matches().is_empty() {
return None;
}
let new_selected = if let Some(mut current) = state.selected() {
current += 1;
if current >= state.number_matches() { 0 } else { current }
}
else {
// select the line after the hint that matches
let mut index_match = 0;
for (i, v) in state.matches().iter().copied().enumerate() {
if v.index() >= state.match_start_hint() {
index_match = i;
break;
}
}
index_match
};
state.set_selected(new_selected);
let new_match_hint = state.match_value(new_selected).map_or(0, |s| s.index());
state.set_match_start_hint(new_match_hint);
Some(new_match_hint)
}
/// Select the previous search result
#[inline]
#[allow(clippy::missing_panics_doc)]
pub(crate) fn previous(&mut self) -> Option<usize> {
let mut state = self.state.write();
if state.matches().is_empty() {
return None;
}
let new_selected = if let Some(current) = state.selected() {
if current == 0 {
state.number_matches().saturating_sub