search_history.rs

  1use std::collections::VecDeque;
  2
  3/// Determines the behavior to use when inserting a new query into the search history.
  4#[derive(Default, Debug, Clone, PartialEq)]
  5pub enum QueryInsertionBehavior {
  6    #[default]
  7    /// Always insert the query to the search history.
  8    AlwaysInsert,
  9    /// Replace the previous query in the search history, if the new query contains the previous query.
 10    ReplacePreviousIfContains,
 11}
 12
 13/// A cursor that stores an index to the currently selected query in the search history.
 14/// This can be passed to the search history to update the selection accordingly,
 15/// e.g. when using the up and down arrow keys to navigate the search history.
 16///
 17/// Note: The cursor can point to the wrong query, if the maximum length of the history is exceeded
 18/// and the old query is overwritten.
 19#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
 20pub struct SearchHistoryCursor {
 21    selection: Option<usize>,
 22    draft: Option<String>,
 23}
 24
 25impl SearchHistoryCursor {
 26    /// Resets the selection to `None` and clears the draft.
 27    pub fn reset(&mut self) {
 28        self.selection = None;
 29        self.draft = None;
 30    }
 31
 32    /// Takes the stored draft query, if any.
 33    pub fn take_draft(&mut self) -> Option<String> {
 34        self.draft.take()
 35    }
 36}
 37
 38#[derive(Debug, Clone)]
 39pub struct SearchHistory {
 40    history: VecDeque<String>,
 41    max_history_len: Option<usize>,
 42    insertion_behavior: QueryInsertionBehavior,
 43}
 44
 45impl SearchHistory {
 46    pub fn new(max_history_len: Option<usize>, insertion_behavior: QueryInsertionBehavior) -> Self {
 47        SearchHistory {
 48            max_history_len,
 49            insertion_behavior,
 50            history: VecDeque::new(),
 51        }
 52    }
 53
 54    pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) {
 55        cursor.draft = None;
 56
 57        if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains
 58            && let Some(previously_searched) = self.history.back_mut()
 59            && search_string.contains(previously_searched.as_str())
 60        {
 61            *previously_searched = search_string;
 62            cursor.selection = Some(self.history.len() - 1);
 63            return;
 64        }
 65
 66        if let Some(max_history_len) = self.max_history_len
 67            && self.history.len() >= max_history_len
 68        {
 69            self.history.pop_front();
 70        }
 71        self.history.push_back(search_string);
 72
 73        cursor.selection = Some(self.history.len() - 1);
 74    }
 75
 76    pub fn next(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
 77        let selected = cursor.selection?;
 78        let next_index = selected + 1;
 79
 80        let next = self.history.get(next_index)?;
 81        cursor.selection = Some(next_index);
 82        Some(next)
 83    }
 84
 85    pub fn current(&self, cursor: &SearchHistoryCursor) -> Option<&str> {
 86        cursor
 87            .selection
 88            .and_then(|selected_ix| self.history.get(selected_ix).map(|s| s.as_str()))
 89    }
 90
 91    /// Get the previous history entry using the given `SearchHistoryCursor`.
 92    /// Uses the last element in the history when there is no cursor.
 93    ///
 94    /// `current_query` is the current text in the search editor. If it differs
 95    /// from the history entry at the cursor position (or if the cursor has no
 96    /// selection), it is saved as a draft so it can be restored later.
 97    pub fn previous(
 98        &mut self,
 99        cursor: &mut SearchHistoryCursor,
100        current_query: &str,
101    ) -> Option<&str> {
102        let matches_history = cursor
103            .selection
104            .and_then(|i| self.history.get(i))
105            .is_some_and(|entry| entry == current_query);
106        if !matches_history {
107            cursor.draft = Some(current_query.to_string());
108        }
109
110        let prev_index = match cursor.selection {
111            Some(index) => index.checked_sub(1)?,
112            None => self.history.len().checked_sub(1)?,
113        };
114
115        let previous = self.history.get(prev_index)?;
116        cursor.selection = Some(prev_index);
117        Some(previous)
118    }
119
120    pub fn len(&self) -> usize {
121        self.history.len()
122    }
123}