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}
 23
 24impl SearchHistoryCursor {
 25    /// Resets the selection to `None`.
 26    pub fn reset(&mut self) {
 27        self.selection = None;
 28    }
 29}
 30
 31#[derive(Debug, Clone)]
 32pub struct SearchHistory {
 33    history: VecDeque<String>,
 34    max_history_len: Option<usize>,
 35    insertion_behavior: QueryInsertionBehavior,
 36}
 37
 38impl SearchHistory {
 39    pub fn new(max_history_len: Option<usize>, insertion_behavior: QueryInsertionBehavior) -> Self {
 40        SearchHistory {
 41            max_history_len,
 42            insertion_behavior,
 43            history: VecDeque::new(),
 44        }
 45    }
 46
 47    pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) {
 48        if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains
 49            && let Some(previously_searched) = self.history.back_mut()
 50            && search_string.contains(previously_searched.as_str())
 51        {
 52            *previously_searched = search_string;
 53            cursor.selection = Some(self.history.len() - 1);
 54            return;
 55        }
 56
 57        if let Some(max_history_len) = self.max_history_len
 58            && self.history.len() >= max_history_len
 59        {
 60            self.history.pop_front();
 61        }
 62        self.history.push_back(search_string);
 63
 64        cursor.selection = Some(self.history.len() - 1);
 65    }
 66
 67    pub fn next(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
 68        let selected = cursor.selection?;
 69        let next_index = selected + 1;
 70
 71        let next = self.history.get(next_index)?;
 72        cursor.selection = Some(next_index);
 73        Some(next)
 74    }
 75
 76    pub fn current(&self, cursor: &SearchHistoryCursor) -> Option<&str> {
 77        cursor
 78            .selection
 79            .and_then(|selected_ix| self.history.get(selected_ix).map(|s| s.as_str()))
 80    }
 81
 82    /// Get the previous history entry using the given `SearchHistoryCursor`.
 83    /// Uses the last element in the history when there is no cursor.
 84    pub fn previous(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
 85        let prev_index = match cursor.selection {
 86            Some(index) => index.checked_sub(1)?,
 87            None => self.history.len().checked_sub(1)?,
 88        };
 89
 90        let previous = self.history.get(prev_index)?;
 91        cursor.selection = Some(prev_index);
 92        Some(previous)
 93    }
 94}
 95
 96#[cfg(test)]
 97mod tests {
 98    use super::*;
 99
100    #[test]
101    fn test_add() {
102        const MAX_HISTORY_LEN: usize = 20;
103        let mut search_history = SearchHistory::new(
104            Some(MAX_HISTORY_LEN),
105            QueryInsertionBehavior::ReplacePreviousIfContains,
106        );
107        let mut cursor = SearchHistoryCursor::default();
108
109        assert_eq!(
110            search_history.current(&cursor),
111            None,
112            "No current selection should be set for the default search history"
113        );
114
115        search_history.add(&mut cursor, "rust".to_string());
116        assert_eq!(
117            search_history.current(&cursor),
118            Some("rust"),
119            "Newly added item should be selected"
120        );
121
122        // check if duplicates are not added
123        search_history.add(&mut cursor, "rust".to_string());
124        assert_eq!(
125            search_history.history.len(),
126            1,
127            "Should not add a duplicate"
128        );
129        assert_eq!(search_history.current(&cursor), Some("rust"));
130
131        // check if new string containing the previous string replaces it
132        search_history.add(&mut cursor, "rustlang".to_string());
133        assert_eq!(
134            search_history.history.len(),
135            1,
136            "Should replace previous item if it's a substring"
137        );
138        assert_eq!(search_history.current(&cursor), Some("rustlang"));
139
140        // add item when it equals to current item if it's not the last one
141        search_history.add(&mut cursor, "php".to_string());
142        search_history.previous(&mut cursor);
143        assert_eq!(search_history.current(&cursor), Some("rustlang"));
144        search_history.add(&mut cursor, "rustlang".to_string());
145        assert_eq!(search_history.history.len(), 3, "Should add item");
146        assert_eq!(search_history.current(&cursor), Some("rustlang"));
147
148        // push enough items to test SEARCH_HISTORY_LIMIT
149        for i in 0..MAX_HISTORY_LEN * 2 {
150            search_history.add(&mut cursor, format!("item{i}"));
151        }
152        assert!(search_history.history.len() <= MAX_HISTORY_LEN);
153    }
154
155    #[test]
156    fn test_next_and_previous() {
157        let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
158        let mut cursor = SearchHistoryCursor::default();
159
160        assert_eq!(
161            search_history.next(&mut cursor),
162            None,
163            "Default search history should not have a next item"
164        );
165
166        search_history.add(&mut cursor, "Rust".to_string());
167        assert_eq!(search_history.next(&mut cursor), None);
168        search_history.add(&mut cursor, "JavaScript".to_string());
169        assert_eq!(search_history.next(&mut cursor), None);
170        search_history.add(&mut cursor, "TypeScript".to_string());
171        assert_eq!(search_history.next(&mut cursor), None);
172
173        assert_eq!(search_history.current(&cursor), Some("TypeScript"));
174
175        assert_eq!(search_history.previous(&mut cursor), Some("JavaScript"));
176        assert_eq!(search_history.current(&cursor), Some("JavaScript"));
177
178        assert_eq!(search_history.previous(&mut cursor), Some("Rust"));
179        assert_eq!(search_history.current(&cursor), Some("Rust"));
180
181        assert_eq!(search_history.previous(&mut cursor), None);
182        assert_eq!(search_history.current(&cursor), Some("Rust"));
183
184        assert_eq!(search_history.next(&mut cursor), Some("JavaScript"));
185        assert_eq!(search_history.current(&cursor), Some("JavaScript"));
186
187        assert_eq!(search_history.next(&mut cursor), Some("TypeScript"));
188        assert_eq!(search_history.current(&cursor), Some("TypeScript"));
189
190        assert_eq!(search_history.next(&mut cursor), None);
191        assert_eq!(search_history.current(&cursor), Some("TypeScript"));
192    }
193
194    #[test]
195    fn test_reset_selection() {
196        let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
197        let mut cursor = SearchHistoryCursor::default();
198
199        search_history.add(&mut cursor, "Rust".to_string());
200        search_history.add(&mut cursor, "JavaScript".to_string());
201        search_history.add(&mut cursor, "TypeScript".to_string());
202
203        assert_eq!(search_history.current(&cursor), Some("TypeScript"));
204        cursor.reset();
205        assert_eq!(search_history.current(&cursor), None);
206        assert_eq!(
207            search_history.previous(&mut cursor),
208            Some("TypeScript"),
209            "Should start from the end after reset on previous item query"
210        );
211
212        search_history.previous(&mut cursor);
213        assert_eq!(search_history.current(&cursor), Some("JavaScript"));
214        search_history.previous(&mut cursor);
215        assert_eq!(search_history.current(&cursor), Some("Rust"));
216
217        cursor.reset();
218        assert_eq!(search_history.current(&cursor), None);
219    }
220
221    #[test]
222    fn test_multiple_cursors() {
223        let mut search_history = SearchHistory::new(None, QueryInsertionBehavior::AlwaysInsert);
224        let mut cursor1 = SearchHistoryCursor::default();
225        let mut cursor2 = SearchHistoryCursor::default();
226
227        search_history.add(&mut cursor1, "Rust".to_string());
228        search_history.add(&mut cursor1, "JavaScript".to_string());
229        search_history.add(&mut cursor1, "TypeScript".to_string());
230
231        search_history.add(&mut cursor2, "Python".to_string());
232        search_history.add(&mut cursor2, "Java".to_string());
233        search_history.add(&mut cursor2, "C++".to_string());
234
235        assert_eq!(search_history.current(&cursor1), Some("TypeScript"));
236        assert_eq!(search_history.current(&cursor2), Some("C++"));
237
238        assert_eq!(search_history.previous(&mut cursor1), Some("JavaScript"));
239        assert_eq!(search_history.previous(&mut cursor2), Some("Java"));
240
241        assert_eq!(search_history.next(&mut cursor1), Some("TypeScript"));
242        assert_eq!(search_history.next(&mut cursor1), Some("Python"));
243
244        cursor1.reset();
245        cursor2.reset();
246
247        assert_eq!(search_history.current(&cursor1), None);
248        assert_eq!(search_history.current(&cursor2), None);
249    }
250}