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