search.rs

  1use bitflags::bitflags;
  2pub use buffer_search::BufferSearchBar;
  3use gpui::{actions, Action, AppContext};
  4use project::search::SearchQuery;
  5pub use project_search::{ProjectSearchBar, ProjectSearchView};
  6use smallvec::SmallVec;
  7
  8pub mod buffer_search;
  9pub mod project_search;
 10
 11pub fn init(cx: &mut AppContext) {
 12    buffer_search::init(cx);
 13    project_search::init(cx);
 14}
 15
 16actions!(
 17    search,
 18    [
 19        ToggleWholeWord,
 20        ToggleCaseSensitive,
 21        ToggleRegex,
 22        SelectNextMatch,
 23        SelectPrevMatch,
 24        SelectAllMatches,
 25        NextHistoryQuery,
 26        PreviousHistoryQuery,
 27    ]
 28);
 29
 30bitflags! {
 31    #[derive(Default)]
 32    pub struct SearchOptions: u8 {
 33        const NONE = 0b000;
 34        const WHOLE_WORD = 0b001;
 35        const CASE_SENSITIVE = 0b010;
 36        const REGEX = 0b100;
 37    }
 38}
 39
 40impl SearchOptions {
 41    pub fn label(&self) -> &'static str {
 42        match *self {
 43            SearchOptions::WHOLE_WORD => "Match Whole Word",
 44            SearchOptions::CASE_SENSITIVE => "Match Case",
 45            SearchOptions::REGEX => "Use Regular Expression",
 46            _ => panic!("{:?} is not a named SearchOption", self),
 47        }
 48    }
 49
 50    pub fn to_toggle_action(&self) -> Box<dyn Action> {
 51        match *self {
 52            SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
 53            SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
 54            SearchOptions::REGEX => Box::new(ToggleRegex),
 55            _ => panic!("{:?} is not a named SearchOption", self),
 56        }
 57    }
 58
 59    pub fn none() -> SearchOptions {
 60        SearchOptions::NONE
 61    }
 62
 63    pub fn from_query(query: &SearchQuery) -> SearchOptions {
 64        let mut options = SearchOptions::NONE;
 65        options.set(SearchOptions::WHOLE_WORD, query.whole_word());
 66        options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
 67        options.set(SearchOptions::REGEX, query.is_regex());
 68        options
 69    }
 70}
 71
 72const SEARCH_HISTORY_LIMIT: usize = 20;
 73
 74#[derive(Default, Debug, Clone)]
 75pub struct SearchHistory {
 76    history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
 77    selected: Option<usize>,
 78}
 79
 80impl SearchHistory {
 81    pub fn add(&mut self, search_string: String) {
 82        if let Some(i) = self.selected {
 83            if search_string == self.history[i] {
 84                return;
 85            }
 86        }
 87
 88        if let Some(previously_searched) = self.history.last_mut() {
 89            if search_string.find(previously_searched.as_str()).is_some() {
 90                *previously_searched = search_string;
 91                self.selected = Some(self.history.len() - 1);
 92                return;
 93            }
 94        }
 95
 96        self.history.push(search_string);
 97        if self.history.len() > SEARCH_HISTORY_LIMIT {
 98            self.history.remove(0);
 99        }
100        self.selected = Some(self.history.len() - 1);
101    }
102
103    pub fn next(&mut self) -> Option<&str> {
104        let history_size = self.history.len();
105        if history_size == 0 {
106            return None;
107        }
108
109        let selected = self.selected?;
110        if selected == history_size - 1 {
111            return None;
112        }
113        let next_index = selected + 1;
114        self.selected = Some(next_index);
115        Some(&self.history[next_index])
116    }
117
118    pub fn current(&self) -> Option<&str> {
119        Some(&self.history[self.selected?])
120    }
121
122    pub fn previous(&mut self) -> Option<&str> {
123        let history_size = self.history.len();
124        if history_size == 0 {
125            return None;
126        }
127
128        let prev_index = match self.selected {
129            Some(selected_index) => {
130                if selected_index == 0 {
131                    return None;
132                } else {
133                    selected_index - 1
134                }
135            }
136            None => history_size - 1,
137        };
138
139        self.selected = Some(prev_index);
140        Some(&self.history[prev_index])
141    }
142
143    pub fn reset_selection(&mut self) {
144        self.selected = None;
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_add() {
154        let mut search_history = SearchHistory::default();
155        assert_eq!(
156            search_history.current(),
157            None,
158            "No current selection should be set fo the default search history"
159        );
160
161        search_history.add("rust".to_string());
162        assert_eq!(
163            search_history.current(),
164            Some("rust"),
165            "Newly added item should be selected"
166        );
167
168        // check if duplicates are not added
169        search_history.add("rust".to_string());
170        assert_eq!(
171            search_history.history.len(),
172            1,
173            "Should not add a duplicate"
174        );
175        assert_eq!(search_history.current(), Some("rust"));
176
177        // check if new string containing the previous string replaces it
178        search_history.add("rustlang".to_string());
179        assert_eq!(
180            search_history.history.len(),
181            1,
182            "Should replace previous item if it's a substring"
183        );
184        assert_eq!(search_history.current(), Some("rustlang"));
185
186        // push enough items to test SEARCH_HISTORY_LIMIT
187        for i in 0..SEARCH_HISTORY_LIMIT * 2 {
188            search_history.add(format!("item{i}"));
189        }
190        assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
191    }
192
193    #[test]
194    fn test_next_and_previous() {
195        let mut search_history = SearchHistory::default();
196        assert_eq!(
197            search_history.next(),
198            None,
199            "Default search history should not have a next item"
200        );
201
202        search_history.add("Rust".to_string());
203        assert_eq!(search_history.next(), None);
204        search_history.add("JavaScript".to_string());
205        assert_eq!(search_history.next(), None);
206        search_history.add("TypeScript".to_string());
207        assert_eq!(search_history.next(), None);
208
209        assert_eq!(search_history.current(), Some("TypeScript"));
210
211        assert_eq!(search_history.previous(), Some("JavaScript"));
212        assert_eq!(search_history.current(), Some("JavaScript"));
213
214        assert_eq!(search_history.previous(), Some("Rust"));
215        assert_eq!(search_history.current(), Some("Rust"));
216
217        assert_eq!(search_history.previous(), None);
218        assert_eq!(search_history.current(), Some("Rust"));
219
220        assert_eq!(search_history.next(), Some("JavaScript"));
221        assert_eq!(search_history.current(), Some("JavaScript"));
222
223        assert_eq!(search_history.next(), Some("TypeScript"));
224        assert_eq!(search_history.current(), Some("TypeScript"));
225
226        assert_eq!(search_history.next(), None);
227        assert_eq!(search_history.current(), Some("TypeScript"));
228    }
229
230    #[test]
231    fn test_reset_selection() {
232        let mut search_history = SearchHistory::default();
233        search_history.add("Rust".to_string());
234        search_history.add("JavaScript".to_string());
235        search_history.add("TypeScript".to_string());
236
237        assert_eq!(search_history.current(), Some("TypeScript"));
238        search_history.reset_selection();
239        assert_eq!(search_history.current(), None);
240        assert_eq!(
241            search_history.previous(),
242            Some("TypeScript"),
243            "Should start from the end after reset on previous item query"
244        );
245
246        search_history.previous();
247        assert_eq!(search_history.current(), Some("JavaScript"));
248        search_history.previous();
249        assert_eq!(search_history.current(), Some("Rust"));
250
251        search_history.reset_selection();
252        assert_eq!(search_history.current(), None);
253    }
254}