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