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