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