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}