search_history.rs

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