1use smallvec::SmallVec;
2const SEARCH_HISTORY_LIMIT: usize = 20;
3
4#[derive(Default, Debug, Clone)]
5pub struct SearchHistory {
6 history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
7 selected: Option<usize>,
8}
9
10impl SearchHistory {
11 pub fn add(&mut self, search_string: String) {
12 if let Some(i) = self.selected {
13 if search_string == self.history[i] {
14 return;
15 }
16 }
17
18 if let Some(previously_searched) = self.history.last_mut() {
19 if search_string.find(previously_searched.as_str()).is_some() {
20 *previously_searched = search_string;
21 self.selected = Some(self.history.len() - 1);
22 return;
23 }
24 }
25
26 self.history.push(search_string);
27 if self.history.len() > SEARCH_HISTORY_LIMIT {
28 self.history.remove(0);
29 }
30 self.selected = Some(self.history.len() - 1);
31 }
32
33 pub fn next(&mut self) -> Option<&str> {
34 let history_size = self.history.len();
35 if history_size == 0 {
36 return None;
37 }
38
39 let selected = self.selected?;
40 if selected == history_size - 1 {
41 return None;
42 }
43 let next_index = selected + 1;
44 self.selected = Some(next_index);
45 Some(&self.history[next_index])
46 }
47
48 pub fn current(&self) -> Option<&str> {
49 Some(&self.history[self.selected?])
50 }
51
52 pub fn previous(&mut self) -> Option<&str> {
53 let history_size = self.history.len();
54 if history_size == 0 {
55 return None;
56 }
57
58 let prev_index = match self.selected {
59 Some(selected_index) => {
60 if selected_index == 0 {
61 return None;
62 } else {
63 selected_index - 1
64 }
65 }
66 None => history_size - 1,
67 };
68
69 self.selected = Some(prev_index);
70 Some(&self.history[prev_index])
71 }
72
73 pub fn reset_selection(&mut self) {
74 self.selected = None;
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_add() {
84 let mut search_history = SearchHistory::default();
85 assert_eq!(
86 search_history.current(),
87 None,
88 "No current selection should be set for the default search history"
89 );
90
91 search_history.add("rust".to_string());
92 assert_eq!(
93 search_history.current(),
94 Some("rust"),
95 "Newly added item should be selected"
96 );
97
98 // check if duplicates are not added
99 search_history.add("rust".to_string());
100 assert_eq!(
101 search_history.history.len(),
102 1,
103 "Should not add a duplicate"
104 );
105 assert_eq!(search_history.current(), Some("rust"));
106
107 // check if new string containing the previous string replaces it
108 search_history.add("rustlang".to_string());
109 assert_eq!(
110 search_history.history.len(),
111 1,
112 "Should replace previous item if it's a substring"
113 );
114 assert_eq!(search_history.current(), Some("rustlang"));
115
116 // push enough items to test SEARCH_HISTORY_LIMIT
117 for i in 0..SEARCH_HISTORY_LIMIT * 2 {
118 search_history.add(format!("item{i}"));
119 }
120 assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
121 }
122
123 #[test]
124 fn test_next_and_previous() {
125 let mut search_history = SearchHistory::default();
126 assert_eq!(
127 search_history.next(),
128 None,
129 "Default search history should not have a next item"
130 );
131
132 search_history.add("Rust".to_string());
133 assert_eq!(search_history.next(), None);
134 search_history.add("JavaScript".to_string());
135 assert_eq!(search_history.next(), None);
136 search_history.add("TypeScript".to_string());
137 assert_eq!(search_history.next(), None);
138
139 assert_eq!(search_history.current(), Some("TypeScript"));
140
141 assert_eq!(search_history.previous(), Some("JavaScript"));
142 assert_eq!(search_history.current(), Some("JavaScript"));
143
144 assert_eq!(search_history.previous(), Some("Rust"));
145 assert_eq!(search_history.current(), Some("Rust"));
146
147 assert_eq!(search_history.previous(), None);
148 assert_eq!(search_history.current(), Some("Rust"));
149
150 assert_eq!(search_history.next(), Some("JavaScript"));
151 assert_eq!(search_history.current(), Some("JavaScript"));
152
153 assert_eq!(search_history.next(), Some("TypeScript"));
154 assert_eq!(search_history.current(), Some("TypeScript"));
155
156 assert_eq!(search_history.next(), None);
157 assert_eq!(search_history.current(), Some("TypeScript"));
158 }
159
160 #[test]
161 fn test_reset_selection() {
162 let mut search_history = SearchHistory::default();
163 search_history.add("Rust".to_string());
164 search_history.add("JavaScript".to_string());
165 search_history.add("TypeScript".to_string());
166
167 assert_eq!(search_history.current(), Some("TypeScript"));
168 search_history.reset_selection();
169 assert_eq!(search_history.current(), None);
170 assert_eq!(
171 search_history.previous(),
172 Some("TypeScript"),
173 "Should start from the end after reset on previous item query"
174 );
175
176 search_history.previous();
177 assert_eq!(search_history.current(), Some("JavaScript"));
178 search_history.previous();
179 assert_eq!(search_history.current(), Some("Rust"));
180
181 search_history.reset_selection();
182 assert_eq!(search_history.current(), None);
183 }
184}