1use crate::{
2 history::SearchHistory,
3 mode::{next_mode, SearchMode},
4 search_bar::{render_nav_button, render_search_mode_button},
5 CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
6 SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
7};
8use collections::HashMap;
9use editor::Editor;
10use futures::channel::oneshot;
11use gpui::{
12 actions,
13 elements::*,
14 impl_actions,
15 platform::{CursorStyle, MouseButton},
16 Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
17 WindowContext,
18};
19use project::search::SearchQuery;
20use serde::Deserialize;
21use std::{any::Any, sync::Arc};
22use util::ResultExt;
23use workspace::{
24 item::ItemHandle,
25 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
26 Pane, ToolbarItemLocation, ToolbarItemView,
27};
28
29#[derive(Clone, Deserialize, PartialEq)]
30pub struct Deploy {
31 pub focus: bool,
32}
33
34actions!(buffer_search, [Dismiss, FocusEditor]);
35impl_actions!(buffer_search, [Deploy]);
36
37pub enum Event {
38 UpdateLocation,
39}
40
41pub fn init(cx: &mut AppContext) {
42 cx.add_action(BufferSearchBar::deploy);
43 cx.add_action(BufferSearchBar::dismiss);
44 cx.add_action(BufferSearchBar::focus_editor);
45 cx.add_action(BufferSearchBar::select_next_match);
46 cx.add_action(BufferSearchBar::select_prev_match);
47 cx.add_action(BufferSearchBar::select_all_matches);
48 cx.add_action(BufferSearchBar::select_next_match_on_pane);
49 cx.add_action(BufferSearchBar::select_prev_match_on_pane);
50 cx.add_action(BufferSearchBar::select_all_matches_on_pane);
51 cx.add_action(BufferSearchBar::handle_editor_cancel);
52 cx.add_action(BufferSearchBar::next_history_query);
53 cx.add_action(BufferSearchBar::previous_history_query);
54 cx.add_action(BufferSearchBar::cycle_mode);
55 cx.add_action(BufferSearchBar::cycle_mode_on_pane);
56 add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
57 add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
58}
59
60fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
61 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
62 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
63 search_bar.update(cx, |search_bar, cx| {
64 if search_bar.show(cx) {
65 search_bar.toggle_search_option(option, cx);
66 }
67 });
68 }
69 cx.propagate_action();
70 });
71}
72
73pub struct BufferSearchBar {
74 query_editor: ViewHandle<Editor>,
75 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
76 active_match_index: Option<usize>,
77 active_searchable_item_subscription: Option<Subscription>,
78 searchable_items_with_matches:
79 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
80 pending_search: Option<Task<()>>,
81 search_options: SearchOptions,
82 default_options: SearchOptions,
83 query_contains_error: bool,
84 dismissed: bool,
85 search_history: SearchHistory,
86 current_mode: SearchMode,
87}
88
89impl Entity for BufferSearchBar {
90 type Event = Event;
91}
92
93impl View for BufferSearchBar {
94 fn ui_name() -> &'static str {
95 "BufferSearchBar"
96 }
97
98 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
99 if cx.is_self_focused() {
100 cx.focus(&self.query_editor);
101 }
102 }
103
104 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
105 let theme = theme::current(cx).clone();
106 let query_container_style = if self.query_contains_error {
107 theme.search.invalid_editor
108 } else {
109 theme.search.editor.input.container
110 };
111 let supported_options = self
112 .active_searchable_item
113 .as_ref()
114 .map(|active_searchable_item| active_searchable_item.supported_options())
115 .unwrap_or_default();
116
117 let previous_query_keystrokes =
118 cx.binding_for_action(&PreviousHistoryQuery {})
119 .map(|binding| {
120 binding
121 .keystrokes()
122 .iter()
123 .map(|k| k.to_string())
124 .collect::<Vec<_>>()
125 });
126 let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
127 binding
128 .keystrokes()
129 .iter()
130 .map(|k| k.to_string())
131 .collect::<Vec<_>>()
132 });
133 let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
134 (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
135 format!(
136 "Search ({}/{} for previous/next query)",
137 previous_query_keystrokes.join(" "),
138 next_query_keystrokes.join(" ")
139 )
140 }
141 (None, Some(next_query_keystrokes)) => {
142 format!(
143 "Search ({} for next query)",
144 next_query_keystrokes.join(" ")
145 )
146 }
147 (Some(previous_query_keystrokes), None) => {
148 format!(
149 "Search ({} for previous query)",
150 previous_query_keystrokes.join(" ")
151 )
152 }
153 (None, None) => String::new(),
154 };
155 self.query_editor.update(cx, |editor, cx| {
156 editor.set_placeholder_text(new_placeholder_text, cx);
157 });
158 let search_button_for_mode = |mode, cx: &mut ViewContext<BufferSearchBar>| {
159 let is_active = self.current_mode == mode;
160
161 render_search_mode_button(
162 mode,
163 is_active,
164 move |_, this, cx| {
165 this.activate_search_mode(mode, cx);
166 },
167 cx,
168 )
169 };
170 let render_search_option =
171 |options: bool, icon, option, cx: &mut ViewContext<BufferSearchBar>| {
172 options.then(|| {
173 let is_active = self.search_options.contains(option);
174 crate::search_bar::render_option_button_icon::<Self>(
175 is_active,
176 icon,
177 option.bits as usize,
178 format!("Toggle {}", option.label()),
179 option.to_toggle_action(),
180 move |_, this, cx| {
181 this.toggle_search_option(option, cx);
182 },
183 cx,
184 )
185 })
186 };
187 let match_count = self
188 .active_searchable_item
189 .as_ref()
190 .and_then(|searchable_item| {
191 if self.query(cx).is_empty() {
192 return None;
193 }
194 let matches = self
195 .searchable_items_with_matches
196 .get(&searchable_item.downgrade())?;
197 let message = if let Some(match_ix) = self.active_match_index {
198 format!("{}/{}", match_ix + 1, matches.len())
199 } else {
200 "No matches".to_string()
201 };
202
203 Some(
204 Label::new(message, theme.search.match_index.text.clone())
205 .contained()
206 .with_style(theme.search.match_index.container)
207 .aligned(),
208 )
209 });
210 let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
211 render_nav_button(
212 label,
213 direction,
214 self.active_match_index.is_some(),
215 move |_, this, cx| match direction {
216 Direction::Prev => this.select_prev_match(&Default::default(), cx),
217 Direction::Next => this.select_next_match(&Default::default(), cx),
218 },
219 cx,
220 )
221 };
222
223 let icon_style = theme.search.editor_icon.clone();
224 let side_column_min_width = 165.;
225 let button_height = 32.;
226 let nav_column = Flex::row()
227 .with_child(
228 Flex::row()
229 .with_children(match_count)
230 .constrained()
231 .with_min_width(100.),
232 )
233 .with_child(nav_button_for_direction("<", Direction::Prev, cx))
234 .with_child(nav_button_for_direction(">", Direction::Next, cx))
235 .with_child(self.render_action_button("Select All", cx))
236 .constrained()
237 .with_height(theme.search.search_bar_row_height);
238
239 let query = Flex::row()
240 .with_child(
241 Svg::for_style(icon_style.icon)
242 .contained()
243 .with_style(icon_style.container),
244 )
245 .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
246 .with_child(
247 Flex::row()
248 .with_children(render_search_option(
249 supported_options.case,
250 "icons/case_insensitive_12.svg",
251 SearchOptions::CASE_SENSITIVE,
252 cx,
253 ))
254 .with_children(render_search_option(
255 supported_options.word,
256 "icons/word_search_12.svg",
257 SearchOptions::WHOLE_WORD,
258 cx,
259 ))
260 .flex(1., true)
261 .contained(),
262 )
263 .align_children_center()
264 .flex(1., true);
265 let editor_column = Flex::row()
266 .with_child(
267 query
268 .contained()
269 .with_style(query_container_style)
270 .constrained()
271 .with_min_width(theme.search.editor.min_width)
272 .with_max_width(theme.search.editor.max_width)
273 .flex(1., false),
274 )
275 .contained()
276 .constrained()
277 .with_height(theme.search.search_bar_row_height)
278 .flex(1., false);
279 let mode_column = Flex::row()
280 .with_child(
281 Flex::row()
282 .with_child(search_button_for_mode(SearchMode::Text, cx))
283 .with_child(search_button_for_mode(SearchMode::Regex, cx))
284 .contained()
285 .with_style(theme.search.modes_container),
286 )
287 .with_child(super::search_bar::render_close_button(
288 "Dismiss Buffer Search",
289 &theme.search,
290 cx,
291 |_, this, cx| this.dismiss(&Default::default(), cx),
292 Some(Box::new(Dismiss)),
293 ))
294 .constrained()
295 .with_height(theme.search.search_bar_row_height)
296 .aligned()
297 .right()
298 .constrained()
299 .with_min_width(side_column_min_width)
300 .flex_float();
301 Flex::row()
302 .with_child(editor_column)
303 .with_child(nav_column)
304 .with_child(mode_column)
305 .contained()
306 .with_style(theme.search.container)
307 .into_any_named("search bar")
308 }
309}
310
311impl ToolbarItemView for BufferSearchBar {
312 fn set_active_pane_item(
313 &mut self,
314 item: Option<&dyn ItemHandle>,
315 cx: &mut ViewContext<Self>,
316 ) -> ToolbarItemLocation {
317 cx.notify();
318 self.active_searchable_item_subscription.take();
319 self.active_searchable_item.take();
320 self.pending_search.take();
321
322 if let Some(searchable_item_handle) =
323 item.and_then(|item| item.to_searchable_item_handle(cx))
324 {
325 let this = cx.weak_handle();
326 self.active_searchable_item_subscription =
327 Some(searchable_item_handle.subscribe_to_search_events(
328 cx,
329 Box::new(move |search_event, cx| {
330 if let Some(this) = this.upgrade(cx) {
331 this.update(cx, |this, cx| {
332 this.on_active_searchable_item_event(search_event, cx)
333 });
334 }
335 }),
336 ));
337
338 self.active_searchable_item = Some(searchable_item_handle);
339 let _ = self.update_matches(cx);
340 if !self.dismissed {
341 return ToolbarItemLocation::Secondary;
342 }
343 }
344
345 ToolbarItemLocation::Hidden
346 }
347
348 fn location_for_event(
349 &self,
350 _: &Self::Event,
351 _: ToolbarItemLocation,
352 _: &AppContext,
353 ) -> ToolbarItemLocation {
354 if self.active_searchable_item.is_some() && !self.dismissed {
355 ToolbarItemLocation::Secondary
356 } else {
357 ToolbarItemLocation::Hidden
358 }
359 }
360 fn row_count(&self, _: &ViewContext<Self>) -> usize {
361 2
362 }
363}
364
365impl BufferSearchBar {
366 pub fn new(cx: &mut ViewContext<Self>) -> Self {
367 let query_editor = cx.add_view(|cx| {
368 Editor::auto_height(
369 2,
370 Some(Arc::new(|theme| theme.search.editor.input.clone())),
371 cx,
372 )
373 });
374 cx.subscribe(&query_editor, Self::on_query_editor_event)
375 .detach();
376
377 Self {
378 query_editor,
379 active_searchable_item: None,
380 active_searchable_item_subscription: None,
381 active_match_index: None,
382 searchable_items_with_matches: Default::default(),
383 default_options: SearchOptions::NONE,
384 search_options: SearchOptions::NONE,
385 pending_search: None,
386 query_contains_error: false,
387 dismissed: true,
388 search_history: SearchHistory::default(),
389 current_mode: SearchMode::default(),
390 }
391 }
392
393 pub fn is_dismissed(&self) -> bool {
394 self.dismissed
395 }
396
397 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
398 self.dismissed = true;
399 for searchable_item in self.searchable_items_with_matches.keys() {
400 if let Some(searchable_item) =
401 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
402 {
403 searchable_item.clear_matches(cx);
404 }
405 }
406 if let Some(active_editor) = self.active_searchable_item.as_ref() {
407 cx.focus(active_editor.as_any());
408 }
409 cx.emit(Event::UpdateLocation);
410 cx.notify();
411 }
412
413 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
414 if self.active_searchable_item.is_none() {
415 return false;
416 }
417 self.dismissed = false;
418 cx.notify();
419 cx.emit(Event::UpdateLocation);
420 true
421 }
422
423 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
424 let search = self
425 .query_suggestion(cx)
426 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
427
428 if let Some(search) = search {
429 cx.spawn(|this, mut cx| async move {
430 search.await?;
431 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
432 })
433 .detach_and_log_err(cx);
434 }
435 }
436
437 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
438 if let Some(match_ix) = self.active_match_index {
439 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
440 if let Some(matches) = self
441 .searchable_items_with_matches
442 .get(&active_searchable_item.downgrade())
443 {
444 active_searchable_item.activate_match(match_ix, matches, cx)
445 }
446 }
447 }
448 }
449
450 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
451 self.query_editor.update(cx, |query_editor, cx| {
452 query_editor.select_all(&Default::default(), cx);
453 });
454 }
455
456 pub fn query(&self, cx: &WindowContext) -> String {
457 self.query_editor.read(cx).text(cx)
458 }
459
460 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
461 self.active_searchable_item
462 .as_ref()
463 .map(|searchable_item| searchable_item.query_suggestion(cx))
464 }
465
466 pub fn search(
467 &mut self,
468 query: &str,
469 options: Option<SearchOptions>,
470 cx: &mut ViewContext<Self>,
471 ) -> oneshot::Receiver<()> {
472 let options = options.unwrap_or(self.default_options);
473 if query != self.query(cx) || self.search_options != options {
474 self.query_editor.update(cx, |query_editor, cx| {
475 query_editor.buffer().update(cx, |query_buffer, cx| {
476 let len = query_buffer.len(cx);
477 query_buffer.edit([(0..len, query)], None, cx);
478 });
479 });
480 self.search_options = options;
481 self.query_contains_error = false;
482 self.clear_matches(cx);
483 cx.notify();
484 }
485 self.update_matches(cx)
486 }
487
488 fn render_action_button(
489 &self,
490 icon: &'static str,
491 cx: &mut ViewContext<Self>,
492 ) -> AnyElement<Self> {
493 let tooltip = "Select All Matches";
494 let tooltip_style = theme::current(cx).tooltip.clone();
495 let action_type_id = 0_usize;
496
497 enum ActionButton {}
498 MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
499 let theme = theme::current(cx);
500 let style = theme.search.action_button.style_for(state);
501 Label::new(icon, style.text.clone())
502 .aligned()
503 .contained()
504 .with_style(style.container)
505 })
506 .on_click(MouseButton::Left, move |_, this, cx| {
507 this.select_all_matches(&SelectAllMatches, cx)
508 })
509 .with_cursor_style(CursorStyle::PointingHand)
510 .with_tooltip::<ActionButton>(
511 action_type_id,
512 tooltip.to_string(),
513 Some(Box::new(SelectAllMatches)),
514 tooltip_style,
515 cx,
516 )
517 .into_any()
518 }
519 pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
520 assert_ne!(
521 mode,
522 SearchMode::Semantic,
523 "Semantic search is not supported in buffer search"
524 );
525 if mode == self.current_mode {
526 return;
527 }
528 self.current_mode = mode;
529 let _ = self.update_matches(cx);
530 cx.notify();
531 }
532 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
533 let mut propagate_action = true;
534 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
535 search_bar.update(cx, |search_bar, cx| {
536 if search_bar.show(cx) {
537 search_bar.search_suggested(cx);
538 if action.focus {
539 search_bar.select_query(cx);
540 cx.focus_self();
541 }
542 propagate_action = false;
543 }
544 });
545 }
546
547 if propagate_action {
548 cx.propagate_action();
549 }
550 }
551
552 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
553 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
554 if !search_bar.read(cx).dismissed {
555 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
556 return;
557 }
558 }
559 cx.propagate_action();
560 }
561
562 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
563 if let Some(active_editor) = self.active_searchable_item.as_ref() {
564 cx.focus(active_editor.as_any());
565 }
566 }
567
568 fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
569 self.search_options.toggle(search_option);
570 self.default_options = self.search_options;
571 let _ = self.update_matches(cx);
572 cx.notify();
573 }
574
575 pub fn set_search_options(
576 &mut self,
577 search_options: SearchOptions,
578 cx: &mut ViewContext<Self>,
579 ) {
580 self.search_options = search_options;
581 cx.notify();
582 }
583
584 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
585 self.select_match(Direction::Next, 1, cx);
586 }
587
588 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
589 self.select_match(Direction::Prev, 1, cx);
590 }
591
592 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
593 if !self.dismissed && self.active_match_index.is_some() {
594 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
595 if let Some(matches) = self
596 .searchable_items_with_matches
597 .get(&searchable_item.downgrade())
598 {
599 searchable_item.select_matches(matches, cx);
600 self.focus_editor(&FocusEditor, cx);
601 }
602 }
603 }
604 }
605
606 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
607 if let Some(index) = self.active_match_index {
608 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
609 if let Some(matches) = self
610 .searchable_items_with_matches
611 .get(&searchable_item.downgrade())
612 {
613 let new_match_index = searchable_item
614 .match_index_for_direction(matches, index, direction, count, cx);
615 searchable_item.update_matches(matches, cx);
616 searchable_item.activate_match(new_match_index, matches, cx);
617 }
618 }
619 }
620 }
621
622 fn select_next_match_on_pane(
623 pane: &mut Pane,
624 action: &SelectNextMatch,
625 cx: &mut ViewContext<Pane>,
626 ) {
627 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
628 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
629 }
630 }
631
632 fn select_prev_match_on_pane(
633 pane: &mut Pane,
634 action: &SelectPrevMatch,
635 cx: &mut ViewContext<Pane>,
636 ) {
637 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
638 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
639 }
640 }
641
642 fn select_all_matches_on_pane(
643 pane: &mut Pane,
644 action: &SelectAllMatches,
645 cx: &mut ViewContext<Pane>,
646 ) {
647 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
648 search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
649 }
650 }
651
652 fn on_query_editor_event(
653 &mut self,
654 _: ViewHandle<Editor>,
655 event: &editor::Event,
656 cx: &mut ViewContext<Self>,
657 ) {
658 if let editor::Event::Edited { .. } = event {
659 self.query_contains_error = false;
660 self.clear_matches(cx);
661 let search = self.update_matches(cx);
662 cx.spawn(|this, mut cx| async move {
663 search.await?;
664 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
665 })
666 .detach_and_log_err(cx);
667 }
668 }
669
670 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
671 match event {
672 SearchEvent::MatchesInvalidated => {
673 let _ = self.update_matches(cx);
674 }
675 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
676 }
677 }
678
679 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
680 let mut active_item_matches = None;
681 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
682 if let Some(searchable_item) =
683 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
684 {
685 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
686 active_item_matches = Some((searchable_item.downgrade(), matches));
687 } else {
688 searchable_item.clear_matches(cx);
689 }
690 }
691 }
692
693 self.searchable_items_with_matches
694 .extend(active_item_matches);
695 }
696
697 fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
698 let (done_tx, done_rx) = oneshot::channel();
699 let query = self.query(cx);
700 self.pending_search.take();
701 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
702 if query.is_empty() {
703 self.active_match_index.take();
704 active_searchable_item.clear_matches(cx);
705 let _ = done_tx.send(());
706 cx.notify();
707 } else {
708 let query = if self.current_mode == SearchMode::Regex {
709 match SearchQuery::regex(
710 query,
711 self.search_options.contains(SearchOptions::WHOLE_WORD),
712 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
713 Vec::new(),
714 Vec::new(),
715 ) {
716 Ok(query) => query,
717 Err(_) => {
718 self.query_contains_error = true;
719 cx.notify();
720 return done_rx;
721 }
722 }
723 } else {
724 SearchQuery::text(
725 query,
726 self.search_options.contains(SearchOptions::WHOLE_WORD),
727 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
728 Vec::new(),
729 Vec::new(),
730 )
731 };
732
733 let query_text = query.as_str().to_string();
734 let matches = active_searchable_item.find_matches(query, cx);
735
736 let active_searchable_item = active_searchable_item.downgrade();
737 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
738 let matches = matches.await;
739 this.update(&mut cx, |this, cx| {
740 if let Some(active_searchable_item) =
741 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
742 {
743 this.searchable_items_with_matches
744 .insert(active_searchable_item.downgrade(), matches);
745
746 this.update_match_index(cx);
747 this.search_history.add(query_text);
748 if !this.dismissed {
749 let matches = this
750 .searchable_items_with_matches
751 .get(&active_searchable_item.downgrade())
752 .unwrap();
753 active_searchable_item.update_matches(matches, cx);
754 let _ = done_tx.send(());
755 }
756 cx.notify();
757 }
758 })
759 .log_err();
760 }));
761 }
762 }
763 done_rx
764 }
765
766 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
767 let new_index = self
768 .active_searchable_item
769 .as_ref()
770 .and_then(|searchable_item| {
771 let matches = self
772 .searchable_items_with_matches
773 .get(&searchable_item.downgrade())?;
774 searchable_item.active_match_index(matches, cx)
775 });
776 if new_index != self.active_match_index {
777 self.active_match_index = new_index;
778 cx.notify();
779 }
780 }
781
782 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
783 if let Some(new_query) = self.search_history.next().map(str::to_string) {
784 let _ = self.search(&new_query, Some(self.search_options), cx);
785 } else {
786 self.search_history.reset_selection();
787 let _ = self.search("", Some(self.search_options), cx);
788 }
789 }
790
791 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
792 if self.query(cx).is_empty() {
793 if let Some(new_query) = self.search_history.current().map(str::to_string) {
794 let _ = self.search(&new_query, Some(self.search_options), cx);
795 return;
796 }
797 }
798
799 if let Some(new_query) = self.search_history.previous().map(str::to_string) {
800 let _ = self.search(&new_query, Some(self.search_options), cx);
801 }
802 }
803 fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
804 self.activate_search_mode(next_mode(&self.current_mode, false), cx);
805 }
806 fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
807 let mut should_propagate = true;
808 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
809 search_bar.update(cx, |bar, cx| {
810 if bar.show(cx) {
811 should_propagate = false;
812 bar.cycle_mode(action, cx);
813 false
814 } else {
815 true
816 }
817 });
818 }
819 if should_propagate {
820 cx.propagate_action();
821 }
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use editor::{DisplayPoint, Editor};
829 use gpui::{color::Color, test::EmptyView, TestAppContext};
830 use language::Buffer;
831 use unindent::Unindent as _;
832
833 fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
834 crate::project_search::tests::init_test(cx);
835
836 let buffer = cx.add_model(|cx| {
837 Buffer::new(
838 0,
839 r#"
840 A regular expression (shortened as regex or regexp;[1] also referred to as
841 rational expression[2][3]) is a sequence of characters that specifies a search
842 pattern in text. Usually such patterns are used by string-searching algorithms
843 for "find" or "find and replace" operations on strings, or for input validation.
844 "#
845 .unindent(),
846 cx,
847 )
848 });
849 let window = cx.add_window(|_| EmptyView);
850 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
851
852 let search_bar = window.add_view(cx, |cx| {
853 let mut search_bar = BufferSearchBar::new(cx);
854 search_bar.set_active_pane_item(Some(&editor), cx);
855 search_bar.show(cx);
856 search_bar
857 });
858
859 (editor, search_bar)
860 }
861
862 #[gpui::test]
863 async fn test_search_simple(cx: &mut TestAppContext) {
864 let (editor, search_bar) = init_test(cx);
865
866 // Search for a string that appears with different casing.
867 // By default, search is case-insensitive.
868 search_bar
869 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
870 .await
871 .unwrap();
872 editor.update(cx, |editor, cx| {
873 assert_eq!(
874 editor.all_background_highlights(cx),
875 &[
876 (
877 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
878 Color::red(),
879 ),
880 (
881 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
882 Color::red(),
883 ),
884 ]
885 );
886 });
887
888 // Switch to a case sensitive search.
889 search_bar.update(cx, |search_bar, cx| {
890 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
891 });
892 editor.next_notification(cx).await;
893 editor.update(cx, |editor, cx| {
894 assert_eq!(
895 editor.all_background_highlights(cx),
896 &[(
897 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
898 Color::red(),
899 )]
900 );
901 });
902
903 // Search for a string that appears both as a whole word and
904 // within other words. By default, all results are found.
905 search_bar
906 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
907 .await
908 .unwrap();
909 editor.update(cx, |editor, cx| {
910 assert_eq!(
911 editor.all_background_highlights(cx),
912 &[
913 (
914 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
915 Color::red(),
916 ),
917 (
918 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
919 Color::red(),
920 ),
921 (
922 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
923 Color::red(),
924 ),
925 (
926 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
927 Color::red(),
928 ),
929 (
930 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
931 Color::red(),
932 ),
933 (
934 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
935 Color::red(),
936 ),
937 (
938 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
939 Color::red(),
940 ),
941 ]
942 );
943 });
944
945 // Switch to a whole word search.
946 search_bar.update(cx, |search_bar, cx| {
947 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
948 });
949 editor.next_notification(cx).await;
950 editor.update(cx, |editor, cx| {
951 assert_eq!(
952 editor.all_background_highlights(cx),
953 &[
954 (
955 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
956 Color::red(),
957 ),
958 (
959 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
960 Color::red(),
961 ),
962 (
963 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
964 Color::red(),
965 ),
966 ]
967 );
968 });
969
970 editor.update(cx, |editor, cx| {
971 editor.change_selections(None, cx, |s| {
972 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
973 });
974 });
975 search_bar.update(cx, |search_bar, cx| {
976 assert_eq!(search_bar.active_match_index, Some(0));
977 search_bar.select_next_match(&SelectNextMatch, cx);
978 assert_eq!(
979 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
980 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
981 );
982 });
983 search_bar.read_with(cx, |search_bar, _| {
984 assert_eq!(search_bar.active_match_index, Some(0));
985 });
986
987 search_bar.update(cx, |search_bar, cx| {
988 search_bar.select_next_match(&SelectNextMatch, cx);
989 assert_eq!(
990 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
991 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
992 );
993 });
994 search_bar.read_with(cx, |search_bar, _| {
995 assert_eq!(search_bar.active_match_index, Some(1));
996 });
997
998 search_bar.update(cx, |search_bar, cx| {
999 search_bar.select_next_match(&SelectNextMatch, cx);
1000 assert_eq!(
1001 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1002 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1003 );
1004 });
1005 search_bar.read_with(cx, |search_bar, _| {
1006 assert_eq!(search_bar.active_match_index, Some(2));
1007 });
1008
1009 search_bar.update(cx, |search_bar, cx| {
1010 search_bar.select_next_match(&SelectNextMatch, cx);
1011 assert_eq!(
1012 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1013 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1014 );
1015 });
1016 search_bar.read_with(cx, |search_bar, _| {
1017 assert_eq!(search_bar.active_match_index, Some(0));
1018 });
1019
1020 search_bar.update(cx, |search_bar, cx| {
1021 search_bar.select_prev_match(&SelectPrevMatch, cx);
1022 assert_eq!(
1023 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1024 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1025 );
1026 });
1027 search_bar.read_with(cx, |search_bar, _| {
1028 assert_eq!(search_bar.active_match_index, Some(2));
1029 });
1030
1031 search_bar.update(cx, |search_bar, cx| {
1032 search_bar.select_prev_match(&SelectPrevMatch, cx);
1033 assert_eq!(
1034 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1035 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1036 );
1037 });
1038 search_bar.read_with(cx, |search_bar, _| {
1039 assert_eq!(search_bar.active_match_index, Some(1));
1040 });
1041
1042 search_bar.update(cx, |search_bar, cx| {
1043 search_bar.select_prev_match(&SelectPrevMatch, cx);
1044 assert_eq!(
1045 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1046 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1047 );
1048 });
1049 search_bar.read_with(cx, |search_bar, _| {
1050 assert_eq!(search_bar.active_match_index, Some(0));
1051 });
1052
1053 // Park the cursor in between matches and ensure that going to the previous match selects
1054 // the closest match to the left.
1055 editor.update(cx, |editor, cx| {
1056 editor.change_selections(None, cx, |s| {
1057 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1058 });
1059 });
1060 search_bar.update(cx, |search_bar, cx| {
1061 assert_eq!(search_bar.active_match_index, Some(1));
1062 search_bar.select_prev_match(&SelectPrevMatch, cx);
1063 assert_eq!(
1064 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1065 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1066 );
1067 });
1068 search_bar.read_with(cx, |search_bar, _| {
1069 assert_eq!(search_bar.active_match_index, Some(0));
1070 });
1071
1072 // Park the cursor in between matches and ensure that going to the next match selects the
1073 // closest match to the right.
1074 editor.update(cx, |editor, cx| {
1075 editor.change_selections(None, cx, |s| {
1076 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1077 });
1078 });
1079 search_bar.update(cx, |search_bar, cx| {
1080 assert_eq!(search_bar.active_match_index, Some(1));
1081 search_bar.select_next_match(&SelectNextMatch, cx);
1082 assert_eq!(
1083 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1084 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1085 );
1086 });
1087 search_bar.read_with(cx, |search_bar, _| {
1088 assert_eq!(search_bar.active_match_index, Some(1));
1089 });
1090
1091 // Park the cursor after the last match and ensure that going to the previous match selects
1092 // the last match.
1093 editor.update(cx, |editor, cx| {
1094 editor.change_selections(None, cx, |s| {
1095 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1096 });
1097 });
1098 search_bar.update(cx, |search_bar, cx| {
1099 assert_eq!(search_bar.active_match_index, Some(2));
1100 search_bar.select_prev_match(&SelectPrevMatch, cx);
1101 assert_eq!(
1102 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1103 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1104 );
1105 });
1106 search_bar.read_with(cx, |search_bar, _| {
1107 assert_eq!(search_bar.active_match_index, Some(2));
1108 });
1109
1110 // Park the cursor after the last match and ensure that going to the next match selects the
1111 // first match.
1112 editor.update(cx, |editor, cx| {
1113 editor.change_selections(None, cx, |s| {
1114 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1115 });
1116 });
1117 search_bar.update(cx, |search_bar, cx| {
1118 assert_eq!(search_bar.active_match_index, Some(2));
1119 search_bar.select_next_match(&SelectNextMatch, cx);
1120 assert_eq!(
1121 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1122 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1123 );
1124 });
1125 search_bar.read_with(cx, |search_bar, _| {
1126 assert_eq!(search_bar.active_match_index, Some(0));
1127 });
1128
1129 // Park the cursor before the first match and ensure that going to the previous match
1130 // selects the last match.
1131 editor.update(cx, |editor, cx| {
1132 editor.change_selections(None, cx, |s| {
1133 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1134 });
1135 });
1136 search_bar.update(cx, |search_bar, cx| {
1137 assert_eq!(search_bar.active_match_index, Some(0));
1138 search_bar.select_prev_match(&SelectPrevMatch, cx);
1139 assert_eq!(
1140 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1141 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1142 );
1143 });
1144 search_bar.read_with(cx, |search_bar, _| {
1145 assert_eq!(search_bar.active_match_index, Some(2));
1146 });
1147 }
1148
1149 #[gpui::test]
1150 async fn test_search_option_handling(cx: &mut TestAppContext) {
1151 let (editor, search_bar) = init_test(cx);
1152
1153 // show with options should make current search case sensitive
1154 search_bar
1155 .update(cx, |search_bar, cx| {
1156 search_bar.show(cx);
1157 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1158 })
1159 .await
1160 .unwrap();
1161 editor.update(cx, |editor, cx| {
1162 assert_eq!(
1163 editor.all_background_highlights(cx),
1164 &[(
1165 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1166 Color::red(),
1167 )]
1168 );
1169 });
1170
1171 // search_suggested should restore default options
1172 search_bar.update(cx, |search_bar, cx| {
1173 search_bar.search_suggested(cx);
1174 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1175 });
1176
1177 // toggling a search option should update the defaults
1178 search_bar
1179 .update(cx, |search_bar, cx| {
1180 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1181 })
1182 .await
1183 .unwrap();
1184 search_bar.update(cx, |search_bar, cx| {
1185 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1186 });
1187 editor.next_notification(cx).await;
1188 editor.update(cx, |editor, cx| {
1189 assert_eq!(
1190 editor.all_background_highlights(cx),
1191 &[(
1192 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1193 Color::red(),
1194 ),]
1195 );
1196 });
1197
1198 // defaults should still include whole word
1199 search_bar.update(cx, |search_bar, cx| {
1200 search_bar.search_suggested(cx);
1201 assert_eq!(
1202 search_bar.search_options,
1203 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1204 )
1205 });
1206 }
1207
1208 #[gpui::test]
1209 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1210 crate::project_search::tests::init_test(cx);
1211
1212 let buffer_text = r#"
1213 A regular expression (shortened as regex or regexp;[1] also referred to as
1214 rational expression[2][3]) is a sequence of characters that specifies a search
1215 pattern in text. Usually such patterns are used by string-searching algorithms
1216 for "find" or "find and replace" operations on strings, or for input validation.
1217 "#
1218 .unindent();
1219 let expected_query_matches_count = buffer_text
1220 .chars()
1221 .filter(|c| c.to_ascii_lowercase() == 'a')
1222 .count();
1223 assert!(
1224 expected_query_matches_count > 1,
1225 "Should pick a query with multiple results"
1226 );
1227 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1228 let window = cx.add_window(|_| EmptyView);
1229 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1230
1231 let search_bar = window.add_view(cx, |cx| {
1232 let mut search_bar = BufferSearchBar::new(cx);
1233 search_bar.set_active_pane_item(Some(&editor), cx);
1234 search_bar.show(cx);
1235 search_bar
1236 });
1237
1238 search_bar
1239 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1240 .await
1241 .unwrap();
1242 search_bar.update(cx, |search_bar, cx| {
1243 cx.focus(search_bar.query_editor.as_any());
1244 search_bar.activate_current_match(cx);
1245 });
1246
1247 window.read_with(cx, |cx| {
1248 assert!(
1249 !editor.is_focused(cx),
1250 "Initially, the editor should not be focused"
1251 );
1252 });
1253
1254 let initial_selections = editor.update(cx, |editor, cx| {
1255 let initial_selections = editor.selections.display_ranges(cx);
1256 assert_eq!(
1257 initial_selections.len(), 1,
1258 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1259 );
1260 initial_selections
1261 });
1262 search_bar.update(cx, |search_bar, _| {
1263 assert_eq!(search_bar.active_match_index, Some(0));
1264 });
1265
1266 search_bar.update(cx, |search_bar, cx| {
1267 cx.focus(search_bar.query_editor.as_any());
1268 search_bar.select_all_matches(&SelectAllMatches, cx);
1269 });
1270 window.read_with(cx, |cx| {
1271 assert!(
1272 editor.is_focused(cx),
1273 "Should focus editor after successful SelectAllMatches"
1274 );
1275 });
1276 search_bar.update(cx, |search_bar, cx| {
1277 let all_selections =
1278 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1279 assert_eq!(
1280 all_selections.len(),
1281 expected_query_matches_count,
1282 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1283 );
1284 assert_eq!(
1285 search_bar.active_match_index,
1286 Some(0),
1287 "Match index should not change after selecting all matches"
1288 );
1289 });
1290
1291 search_bar.update(cx, |search_bar, cx| {
1292 search_bar.select_next_match(&SelectNextMatch, cx);
1293 });
1294 window.read_with(cx, |cx| {
1295 assert!(
1296 editor.is_focused(cx),
1297 "Should still have editor focused after SelectNextMatch"
1298 );
1299 });
1300 search_bar.update(cx, |search_bar, cx| {
1301 let all_selections =
1302 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1303 assert_eq!(
1304 all_selections.len(),
1305 1,
1306 "On next match, should deselect items and select the next match"
1307 );
1308 assert_ne!(
1309 all_selections, initial_selections,
1310 "Next match should be different from the first selection"
1311 );
1312 assert_eq!(
1313 search_bar.active_match_index,
1314 Some(1),
1315 "Match index should be updated to the next one"
1316 );
1317 });
1318
1319 search_bar.update(cx, |search_bar, cx| {
1320 cx.focus(search_bar.query_editor.as_any());
1321 search_bar.select_all_matches(&SelectAllMatches, cx);
1322 });
1323 window.read_with(cx, |cx| {
1324 assert!(
1325 editor.is_focused(cx),
1326 "Should focus editor after successful SelectAllMatches"
1327 );
1328 });
1329 search_bar.update(cx, |search_bar, cx| {
1330 let all_selections =
1331 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1332 assert_eq!(
1333 all_selections.len(),
1334 expected_query_matches_count,
1335 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1336 );
1337 assert_eq!(
1338 search_bar.active_match_index,
1339 Some(1),
1340 "Match index should not change after selecting all matches"
1341 );
1342 });
1343
1344 search_bar.update(cx, |search_bar, cx| {
1345 search_bar.select_prev_match(&SelectPrevMatch, cx);
1346 });
1347 window.read_with(cx, |cx| {
1348 assert!(
1349 editor.is_focused(cx),
1350 "Should still have editor focused after SelectPrevMatch"
1351 );
1352 });
1353 let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1354 let all_selections =
1355 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1356 assert_eq!(
1357 all_selections.len(),
1358 1,
1359 "On previous match, should deselect items and select the previous item"
1360 );
1361 assert_eq!(
1362 all_selections, initial_selections,
1363 "Previous match should be the same as the first selection"
1364 );
1365 assert_eq!(
1366 search_bar.active_match_index,
1367 Some(0),
1368 "Match index should be updated to the previous one"
1369 );
1370 all_selections
1371 });
1372
1373 search_bar
1374 .update(cx, |search_bar, cx| {
1375 cx.focus(search_bar.query_editor.as_any());
1376 search_bar.search("abas_nonexistent_match", None, cx)
1377 })
1378 .await
1379 .unwrap();
1380 search_bar.update(cx, |search_bar, cx| {
1381 search_bar.select_all_matches(&SelectAllMatches, cx);
1382 });
1383 window.read_with(cx, |cx| {
1384 assert!(
1385 !editor.is_focused(cx),
1386 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1387 );
1388 });
1389 search_bar.update(cx, |search_bar, cx| {
1390 let all_selections =
1391 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1392 assert_eq!(
1393 all_selections, last_match_selections,
1394 "Should not select anything new if there are no matches"
1395 );
1396 assert!(
1397 search_bar.active_match_index.is_none(),
1398 "For no matches, there should be no active match index"
1399 );
1400 });
1401 }
1402
1403 #[gpui::test]
1404 async fn test_search_query_history(cx: &mut TestAppContext) {
1405 crate::project_search::tests::init_test(cx);
1406
1407 let buffer_text = r#"
1408 A regular expression (shortened as regex or regexp;[1] also referred to as
1409 rational expression[2][3]) is a sequence of characters that specifies a search
1410 pattern in text. Usually such patterns are used by string-searching algorithms
1411 for "find" or "find and replace" operations on strings, or for input validation.
1412 "#
1413 .unindent();
1414 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1415 let window = cx.add_window(|_| EmptyView);
1416
1417 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1418
1419 let search_bar = window.add_view(cx, |cx| {
1420 let mut search_bar = BufferSearchBar::new(cx);
1421 search_bar.set_active_pane_item(Some(&editor), cx);
1422 search_bar.show(cx);
1423 search_bar
1424 });
1425
1426 // Add 3 search items into the history.
1427 search_bar
1428 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1429 .await
1430 .unwrap();
1431 search_bar
1432 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1433 .await
1434 .unwrap();
1435 search_bar
1436 .update(cx, |search_bar, cx| {
1437 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1438 })
1439 .await
1440 .unwrap();
1441 // Ensure that the latest search is active.
1442 search_bar.read_with(cx, |search_bar, cx| {
1443 assert_eq!(search_bar.query(cx), "c");
1444 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1445 });
1446
1447 // Next history query after the latest should set the query to the empty string.
1448 search_bar.update(cx, |search_bar, cx| {
1449 search_bar.next_history_query(&NextHistoryQuery, cx);
1450 });
1451 search_bar.read_with(cx, |search_bar, cx| {
1452 assert_eq!(search_bar.query(cx), "");
1453 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1454 });
1455 search_bar.update(cx, |search_bar, cx| {
1456 search_bar.next_history_query(&NextHistoryQuery, cx);
1457 });
1458 search_bar.read_with(cx, |search_bar, cx| {
1459 assert_eq!(search_bar.query(cx), "");
1460 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1461 });
1462
1463 // First previous query for empty current query should set the query to the latest.
1464 search_bar.update(cx, |search_bar, cx| {
1465 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1466 });
1467 search_bar.read_with(cx, |search_bar, cx| {
1468 assert_eq!(search_bar.query(cx), "c");
1469 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1470 });
1471
1472 // Further previous items should go over the history in reverse order.
1473 search_bar.update(cx, |search_bar, cx| {
1474 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1475 });
1476 search_bar.read_with(cx, |search_bar, cx| {
1477 assert_eq!(search_bar.query(cx), "b");
1478 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1479 });
1480
1481 // Previous items should never go behind the first history item.
1482 search_bar.update(cx, |search_bar, cx| {
1483 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1484 });
1485 search_bar.read_with(cx, |search_bar, cx| {
1486 assert_eq!(search_bar.query(cx), "a");
1487 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1488 });
1489 search_bar.update(cx, |search_bar, cx| {
1490 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1491 });
1492 search_bar.read_with(cx, |search_bar, cx| {
1493 assert_eq!(search_bar.query(cx), "a");
1494 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1495 });
1496
1497 // Next items should go over the history in the original order.
1498 search_bar.update(cx, |search_bar, cx| {
1499 search_bar.next_history_query(&NextHistoryQuery, cx);
1500 });
1501 search_bar.read_with(cx, |search_bar, cx| {
1502 assert_eq!(search_bar.query(cx), "b");
1503 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1504 });
1505
1506 search_bar
1507 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1508 .await
1509 .unwrap();
1510 search_bar.read_with(cx, |search_bar, cx| {
1511 assert_eq!(search_bar.query(cx), "ba");
1512 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1513 });
1514
1515 // New search input should add another entry to history and move the selection to the end of the history.
1516 search_bar.update(cx, |search_bar, cx| {
1517 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1518 });
1519 search_bar.read_with(cx, |search_bar, cx| {
1520 assert_eq!(search_bar.query(cx), "c");
1521 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1522 });
1523 search_bar.update(cx, |search_bar, cx| {
1524 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1525 });
1526 search_bar.read_with(cx, |search_bar, cx| {
1527 assert_eq!(search_bar.query(cx), "b");
1528 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1529 });
1530 search_bar.update(cx, |search_bar, cx| {
1531 search_bar.next_history_query(&NextHistoryQuery, cx);
1532 });
1533 search_bar.read_with(cx, |search_bar, cx| {
1534 assert_eq!(search_bar.query(cx), "c");
1535 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1536 });
1537 search_bar.update(cx, |search_bar, cx| {
1538 search_bar.next_history_query(&NextHistoryQuery, cx);
1539 });
1540 search_bar.read_with(cx, |search_bar, cx| {
1541 assert_eq!(search_bar.query(cx), "ba");
1542 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1543 });
1544 search_bar.update(cx, |search_bar, cx| {
1545 search_bar.next_history_query(&NextHistoryQuery, cx);
1546 });
1547 search_bar.read_with(cx, |search_bar, cx| {
1548 assert_eq!(search_bar.query(cx), "");
1549 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1550 });
1551 }
1552}