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}