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