history.rs

  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}