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