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