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}