1use crate::{
2 SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
3 ToggleRegex, ToggleWholeWord,
4};
5use collections::HashMap;
6use editor::Editor;
7use gpui::{
8 actions,
9 elements::*,
10 impl_actions,
11 platform::{CursorStyle, MouseButton},
12 Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
13};
14use project::search::SearchQuery;
15use serde::Deserialize;
16use std::{any::Any, sync::Arc};
17use util::ResultExt;
18use workspace::{
19 item::ItemHandle,
20 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
21 Pane, ToolbarItemLocation, ToolbarItemView,
22};
23
24#[derive(Clone, Deserialize, PartialEq)]
25pub struct Deploy {
26 pub focus: bool,
27}
28
29actions!(buffer_search, [Dismiss, FocusEditor]);
30impl_actions!(buffer_search, [Deploy]);
31
32pub enum Event {
33 UpdateLocation,
34}
35
36pub fn init(cx: &mut AppContext) {
37 cx.add_action(BufferSearchBar::deploy);
38 cx.add_action(BufferSearchBar::dismiss);
39 cx.add_action(BufferSearchBar::focus_editor);
40 cx.add_action(BufferSearchBar::select_next_match);
41 cx.add_action(BufferSearchBar::select_prev_match);
42 cx.add_action(BufferSearchBar::select_all_matches);
43 cx.add_action(BufferSearchBar::select_next_match_on_pane);
44 cx.add_action(BufferSearchBar::select_prev_match_on_pane);
45 cx.add_action(BufferSearchBar::select_all_matches_on_pane);
46 cx.add_action(BufferSearchBar::handle_editor_cancel);
47 add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
48 add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
49 add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
50}
51
52fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
53 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
54 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
55 if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
56 search_bar.update(cx, |search_bar, cx| {
57 search_bar.toggle_search_option(option, cx);
58 });
59 return;
60 }
61 }
62 cx.propagate_action();
63 });
64}
65
66pub struct BufferSearchBar {
67 pub query_editor: ViewHandle<Editor>,
68 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
69 active_match_index: Option<usize>,
70 active_searchable_item_subscription: Option<Subscription>,
71 searchable_items_with_matches:
72 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
73 pending_search: Option<Task<()>>,
74 case_sensitive: bool,
75 whole_word: bool,
76 regex: bool,
77 query_contains_error: bool,
78 dismissed: bool,
79}
80
81impl Entity for BufferSearchBar {
82 type Event = Event;
83}
84
85impl View for BufferSearchBar {
86 fn ui_name() -> &'static str {
87 "BufferSearchBar"
88 }
89
90 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
91 if cx.is_self_focused() {
92 cx.focus(&self.query_editor);
93 }
94 }
95
96 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
97 let theme = theme::current(cx).clone();
98 let editor_container = if self.query_contains_error {
99 theme.search.invalid_editor
100 } else {
101 theme.search.editor.input.container
102 };
103 let supported_options = self
104 .active_searchable_item
105 .as_ref()
106 .map(|active_searchable_item| active_searchable_item.supported_options())
107 .unwrap_or_default();
108
109 Flex::row()
110 .with_child(
111 Flex::row()
112 .with_child(
113 Flex::row()
114 .with_child(
115 ChildView::new(&self.query_editor, cx)
116 .aligned()
117 .left()
118 .flex(1., true),
119 )
120 .with_children(self.active_searchable_item.as_ref().and_then(
121 |searchable_item| {
122 let matches = self
123 .searchable_items_with_matches
124 .get(&searchable_item.downgrade())?;
125 let message = if let Some(match_ix) = self.active_match_index {
126 format!("{}/{}", match_ix + 1, matches.len())
127 } else {
128 "No matches".to_string()
129 };
130
131 Some(
132 Label::new(message, theme.search.match_index.text.clone())
133 .contained()
134 .with_style(theme.search.match_index.container)
135 .aligned(),
136 )
137 },
138 ))
139 .contained()
140 .with_style(editor_container)
141 .aligned()
142 .constrained()
143 .with_min_width(theme.search.editor.min_width)
144 .with_max_width(theme.search.editor.max_width)
145 .flex(1., false),
146 )
147 .with_child(
148 Flex::row()
149 .with_child(self.render_nav_button("<", Direction::Prev, cx))
150 .with_child(self.render_nav_button(">", Direction::Next, cx))
151 .aligned(),
152 )
153 .with_child(
154 Flex::row()
155 .with_children(self.render_search_option(
156 supported_options.case,
157 "Case",
158 SearchOption::CaseSensitive,
159 cx,
160 ))
161 .with_children(self.render_search_option(
162 supported_options.word,
163 "Word",
164 SearchOption::WholeWord,
165 cx,
166 ))
167 .with_children(self.render_search_option(
168 supported_options.regex,
169 "Regex",
170 SearchOption::Regex,
171 cx,
172 ))
173 .contained()
174 .with_style(theme.search.option_button_group)
175 .aligned(),
176 )
177 .flex(1., true),
178 )
179 .with_child(self.render_close_button(&theme.search, cx))
180 .contained()
181 .with_style(theme.search.container)
182 .into_any_named("search bar")
183 }
184}
185
186impl ToolbarItemView for BufferSearchBar {
187 fn set_active_pane_item(
188 &mut self,
189 item: Option<&dyn ItemHandle>,
190 cx: &mut ViewContext<Self>,
191 ) -> ToolbarItemLocation {
192 cx.notify();
193 self.active_searchable_item_subscription.take();
194 self.active_searchable_item.take();
195 self.pending_search.take();
196
197 if let Some(searchable_item_handle) =
198 item.and_then(|item| item.to_searchable_item_handle(cx))
199 {
200 let this = cx.weak_handle();
201 self.active_searchable_item_subscription =
202 Some(searchable_item_handle.subscribe_to_search_events(
203 cx,
204 Box::new(move |search_event, cx| {
205 if let Some(this) = this.upgrade(cx) {
206 this.update(cx, |this, cx| {
207 this.on_active_searchable_item_event(search_event, cx)
208 });
209 }
210 }),
211 ));
212
213 self.active_searchable_item = Some(searchable_item_handle);
214 self.update_matches(false, cx);
215 if !self.dismissed {
216 return ToolbarItemLocation::Secondary;
217 }
218 }
219
220 ToolbarItemLocation::Hidden
221 }
222
223 fn location_for_event(
224 &self,
225 _: &Self::Event,
226 _: ToolbarItemLocation,
227 _: &AppContext,
228 ) -> ToolbarItemLocation {
229 if self.active_searchable_item.is_some() && !self.dismissed {
230 ToolbarItemLocation::Secondary
231 } else {
232 ToolbarItemLocation::Hidden
233 }
234 }
235}
236
237impl BufferSearchBar {
238 pub fn new(cx: &mut ViewContext<Self>) -> Self {
239 let query_editor = cx.add_view(|cx| {
240 Editor::auto_height(
241 2,
242 Some(Arc::new(|theme| theme.search.editor.input.clone())),
243 cx,
244 )
245 });
246 cx.subscribe(&query_editor, Self::on_query_editor_event)
247 .detach();
248
249 Self {
250 query_editor,
251 active_searchable_item: None,
252 active_searchable_item_subscription: None,
253 active_match_index: None,
254 searchable_items_with_matches: Default::default(),
255 case_sensitive: false,
256 whole_word: false,
257 regex: false,
258 pending_search: None,
259 query_contains_error: false,
260 dismissed: true,
261 }
262 }
263
264 pub fn is_dismissed(&self) -> bool {
265 self.dismissed
266 }
267
268 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
269 self.dismissed = true;
270 for searchable_item in self.searchable_items_with_matches.keys() {
271 if let Some(searchable_item) =
272 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
273 {
274 searchable_item.clear_matches(cx);
275 }
276 }
277 if let Some(active_editor) = self.active_searchable_item.as_ref() {
278 cx.focus(active_editor.as_any());
279 }
280 cx.emit(Event::UpdateLocation);
281 cx.notify();
282 }
283
284 pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
285 let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
286 SearchableItemHandle::boxed_clone(searchable_item.as_ref())
287 } else {
288 return false;
289 };
290
291 if suggest_query {
292 let text = searchable_item.query_suggestion(cx);
293 if !text.is_empty() {
294 self.set_query(&text, cx);
295 }
296 }
297
298 if focus {
299 let query_editor = self.query_editor.clone();
300 query_editor.update(cx, |query_editor, cx| {
301 query_editor.select_all(&editor::SelectAll, cx);
302 });
303 cx.focus_self();
304 }
305
306 self.dismissed = false;
307 cx.notify();
308 cx.emit(Event::UpdateLocation);
309 true
310 }
311
312 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
313 self.query_editor.update(cx, |query_editor, cx| {
314 query_editor.buffer().update(cx, |query_buffer, cx| {
315 let len = query_buffer.len(cx);
316 query_buffer.edit([(0..len, query)], None, cx);
317 });
318 });
319 }
320
321 fn render_search_option(
322 &self,
323 option_supported: bool,
324 icon: &'static str,
325 option: SearchOption,
326 cx: &mut ViewContext<Self>,
327 ) -> Option<AnyElement<Self>> {
328 if !option_supported {
329 return None;
330 }
331
332 let tooltip_style = theme::current(cx).tooltip.clone();
333 let is_active = self.is_search_option_enabled(option);
334 Some(
335 MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
336 let theme = theme::current(cx);
337 let style = theme
338 .search
339 .option_button
340 .in_state(is_active)
341 .style_for(state);
342 Label::new(icon, style.text.clone())
343 .contained()
344 .with_style(style.container)
345 })
346 .on_click(MouseButton::Left, move |_, this, cx| {
347 this.toggle_search_option(option, cx);
348 })
349 .with_cursor_style(CursorStyle::PointingHand)
350 .with_tooltip::<Self>(
351 option as usize,
352 format!("Toggle {}", option.label()),
353 Some(option.to_toggle_action()),
354 tooltip_style,
355 cx,
356 )
357 .into_any(),
358 )
359 }
360
361 fn render_nav_button(
362 &self,
363 icon: &'static str,
364 direction: Direction,
365 cx: &mut ViewContext<Self>,
366 ) -> AnyElement<Self> {
367 let action: Box<dyn Action>;
368 let tooltip;
369 match direction {
370 Direction::Prev => {
371 action = Box::new(SelectPrevMatch);
372 tooltip = "Select Previous Match";
373 }
374 Direction::Next => {
375 action = Box::new(SelectNextMatch);
376 tooltip = "Select Next Match";
377 }
378 };
379 let tooltip_style = theme::current(cx).tooltip.clone();
380
381 enum NavButton {}
382 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
383 let theme = theme::current(cx);
384 let style = theme.search.option_button.inactive_state().style_for(state);
385 Label::new(icon, style.text.clone())
386 .contained()
387 .with_style(style.container)
388 })
389 .on_click(MouseButton::Left, {
390 move |_, this, cx| match direction {
391 Direction::Prev => this.select_prev_match(&Default::default(), cx),
392 Direction::Next => this.select_next_match(&Default::default(), cx),
393 }
394 })
395 .with_cursor_style(CursorStyle::PointingHand)
396 .with_tooltip::<NavButton>(
397 direction as usize,
398 tooltip.to_string(),
399 Some(action),
400 tooltip_style,
401 cx,
402 )
403 .into_any()
404 }
405
406 fn render_close_button(
407 &self,
408 theme: &theme::Search,
409 cx: &mut ViewContext<Self>,
410 ) -> AnyElement<Self> {
411 let tooltip = "Dismiss Buffer Search";
412 let tooltip_style = theme::current(cx).tooltip.clone();
413
414 enum CloseButton {}
415 MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
416 let style = theme.dismiss_button.style_for(state);
417 Svg::new("icons/x_mark_8.svg")
418 .with_color(style.color)
419 .constrained()
420 .with_width(style.icon_width)
421 .aligned()
422 .constrained()
423 .with_width(style.button_width)
424 .contained()
425 .with_style(style.container)
426 })
427 .on_click(MouseButton::Left, move |_, this, cx| {
428 this.dismiss(&Default::default(), cx)
429 })
430 .with_cursor_style(CursorStyle::PointingHand)
431 .with_tooltip::<CloseButton>(
432 0,
433 tooltip.to_string(),
434 Some(Box::new(Dismiss)),
435 tooltip_style,
436 cx,
437 )
438 .into_any()
439 }
440
441 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
442 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
443 if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
444 return;
445 }
446 }
447 cx.propagate_action();
448 }
449
450 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
451 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
452 if !search_bar.read(cx).dismissed {
453 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
454 return;
455 }
456 }
457 cx.propagate_action();
458 }
459
460 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
461 if let Some(active_editor) = self.active_searchable_item.as_ref() {
462 cx.focus(active_editor.as_any());
463 }
464 }
465
466 fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
467 match search_option {
468 SearchOption::WholeWord => self.whole_word,
469 SearchOption::CaseSensitive => self.case_sensitive,
470 SearchOption::Regex => self.regex,
471 }
472 }
473
474 fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
475 let value = match search_option {
476 SearchOption::WholeWord => &mut self.whole_word,
477 SearchOption::CaseSensitive => &mut self.case_sensitive,
478 SearchOption::Regex => &mut self.regex,
479 };
480 *value = !*value;
481 self.update_matches(false, cx);
482 cx.notify();
483 }
484
485 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
486 self.select_match(Direction::Next, cx);
487 }
488
489 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
490 self.select_match(Direction::Prev, cx);
491 }
492
493 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
494 if !self.dismissed {
495 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
496 if let Some(matches) = self
497 .searchable_items_with_matches
498 .get(&searchable_item.downgrade())
499 {
500 searchable_item.select_matches(matches, cx);
501 self.focus_editor(&FocusEditor, cx);
502 }
503 }
504 }
505 }
506
507 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
508 if let Some(index) = self.active_match_index {
509 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
510 if let Some(matches) = self
511 .searchable_items_with_matches
512 .get(&searchable_item.downgrade())
513 {
514 let new_match_index =
515 searchable_item.match_index_for_direction(matches, index, direction, cx);
516 searchable_item.update_matches(matches, cx);
517 searchable_item.activate_match(new_match_index, matches, cx);
518 }
519 }
520 }
521 }
522
523 fn select_next_match_on_pane(
524 pane: &mut Pane,
525 action: &SelectNextMatch,
526 cx: &mut ViewContext<Pane>,
527 ) {
528 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
529 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
530 }
531 }
532
533 fn select_prev_match_on_pane(
534 pane: &mut Pane,
535 action: &SelectPrevMatch,
536 cx: &mut ViewContext<Pane>,
537 ) {
538 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
539 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
540 }
541 }
542
543 fn select_all_matches_on_pane(
544 pane: &mut Pane,
545 action: &SelectAllMatches,
546 cx: &mut ViewContext<Pane>,
547 ) {
548 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
549 search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
550 }
551 }
552
553 fn on_query_editor_event(
554 &mut self,
555 _: ViewHandle<Editor>,
556 event: &editor::Event,
557 cx: &mut ViewContext<Self>,
558 ) {
559 if let editor::Event::BufferEdited { .. } = event {
560 self.query_contains_error = false;
561 self.clear_matches(cx);
562 self.update_matches(true, cx);
563 cx.notify();
564 }
565 }
566
567 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
568 match event {
569 SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
570 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
571 }
572 }
573
574 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
575 let mut active_item_matches = None;
576 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
577 if let Some(searchable_item) =
578 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
579 {
580 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
581 active_item_matches = Some((searchable_item.downgrade(), matches));
582 } else {
583 searchable_item.clear_matches(cx);
584 }
585 }
586 }
587
588 self.searchable_items_with_matches
589 .extend(active_item_matches);
590 }
591
592 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
593 let query = self.query_editor.read(cx).text(cx);
594 self.pending_search.take();
595 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
596 if query.is_empty() {
597 self.active_match_index.take();
598 active_searchable_item.clear_matches(cx);
599 } else {
600 let query = if self.regex {
601 match SearchQuery::regex(
602 query,
603 self.whole_word,
604 self.case_sensitive,
605 Vec::new(),
606 Vec::new(),
607 ) {
608 Ok(query) => query,
609 Err(_) => {
610 self.query_contains_error = true;
611 cx.notify();
612 return;
613 }
614 }
615 } else {
616 SearchQuery::text(
617 query,
618 self.whole_word,
619 self.case_sensitive,
620 Vec::new(),
621 Vec::new(),
622 )
623 };
624
625 let matches = active_searchable_item.find_matches(query, cx);
626
627 let active_searchable_item = active_searchable_item.downgrade();
628 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
629 let matches = matches.await;
630 this.update(&mut cx, |this, cx| {
631 if let Some(active_searchable_item) =
632 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
633 {
634 this.searchable_items_with_matches
635 .insert(active_searchable_item.downgrade(), matches);
636
637 this.update_match_index(cx);
638 if !this.dismissed {
639 let matches = this
640 .searchable_items_with_matches
641 .get(&active_searchable_item.downgrade())
642 .unwrap();
643 active_searchable_item.update_matches(matches, cx);
644 if select_closest_match {
645 if let Some(match_ix) = this.active_match_index {
646 active_searchable_item
647 .activate_match(match_ix, matches, cx);
648 }
649 }
650 }
651 cx.notify();
652 }
653 })
654 .log_err();
655 }));
656 }
657 }
658 }
659
660 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
661 let new_index = self
662 .active_searchable_item
663 .as_ref()
664 .and_then(|searchable_item| {
665 let matches = self
666 .searchable_items_with_matches
667 .get(&searchable_item.downgrade())?;
668 searchable_item.active_match_index(matches, cx)
669 });
670 if new_index != self.active_match_index {
671 self.active_match_index = new_index;
672 cx.notify();
673 }
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use editor::{DisplayPoint, Editor};
681 use gpui::{color::Color, test::EmptyView, TestAppContext};
682 use language::Buffer;
683 use unindent::Unindent as _;
684
685 #[gpui::test]
686 async fn test_search_simple(cx: &mut TestAppContext) {
687 crate::project_search::tests::init_test(cx);
688
689 let buffer = cx.add_model(|cx| {
690 Buffer::new(
691 0,
692 r#"
693 A regular expression (shortened as regex or regexp;[1] also referred to as
694 rational expression[2][3]) is a sequence of characters that specifies a search
695 pattern in text. Usually such patterns are used by string-searching algorithms
696 for "find" or "find and replace" operations on strings, or for input validation.
697 "#
698 .unindent(),
699 cx,
700 )
701 });
702 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
703
704 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
705
706 let search_bar = cx.add_view(window_id, |cx| {
707 let mut search_bar = BufferSearchBar::new(cx);
708 search_bar.set_active_pane_item(Some(&editor), cx);
709 search_bar.show(false, true, cx);
710 search_bar
711 });
712
713 // Search for a string that appears with different casing.
714 // By default, search is case-insensitive.
715 search_bar.update(cx, |search_bar, cx| {
716 search_bar.set_query("us", cx);
717 });
718 editor.next_notification(cx).await;
719 editor.update(cx, |editor, cx| {
720 assert_eq!(
721 editor.all_background_highlights(cx),
722 &[
723 (
724 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
725 Color::red(),
726 ),
727 (
728 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
729 Color::red(),
730 ),
731 ]
732 );
733 });
734
735 // Switch to a case sensitive search.
736 search_bar.update(cx, |search_bar, cx| {
737 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
738 });
739 editor.next_notification(cx).await;
740 editor.update(cx, |editor, cx| {
741 assert_eq!(
742 editor.all_background_highlights(cx),
743 &[(
744 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
745 Color::red(),
746 )]
747 );
748 });
749
750 // Search for a string that appears both as a whole word and
751 // within other words. By default, all results are found.
752 search_bar.update(cx, |search_bar, cx| {
753 search_bar.set_query("or", cx);
754 });
755 editor.next_notification(cx).await;
756 editor.update(cx, |editor, cx| {
757 assert_eq!(
758 editor.all_background_highlights(cx),
759 &[
760 (
761 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
762 Color::red(),
763 ),
764 (
765 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
766 Color::red(),
767 ),
768 (
769 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
770 Color::red(),
771 ),
772 (
773 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
774 Color::red(),
775 ),
776 (
777 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
778 Color::red(),
779 ),
780 (
781 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
782 Color::red(),
783 ),
784 (
785 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
786 Color::red(),
787 ),
788 ]
789 );
790 });
791
792 // Switch to a whole word search.
793 search_bar.update(cx, |search_bar, cx| {
794 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
795 });
796 editor.next_notification(cx).await;
797 editor.update(cx, |editor, cx| {
798 assert_eq!(
799 editor.all_background_highlights(cx),
800 &[
801 (
802 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
803 Color::red(),
804 ),
805 (
806 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
807 Color::red(),
808 ),
809 (
810 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
811 Color::red(),
812 ),
813 ]
814 );
815 });
816
817 editor.update(cx, |editor, cx| {
818 editor.change_selections(None, cx, |s| {
819 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
820 });
821 });
822 search_bar.update(cx, |search_bar, cx| {
823 assert_eq!(search_bar.active_match_index, Some(0));
824 search_bar.select_next_match(&SelectNextMatch, cx);
825 assert_eq!(
826 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
827 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
828 );
829 });
830 search_bar.read_with(cx, |search_bar, _| {
831 assert_eq!(search_bar.active_match_index, Some(0));
832 });
833
834 search_bar.update(cx, |search_bar, cx| {
835 search_bar.select_next_match(&SelectNextMatch, cx);
836 assert_eq!(
837 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
838 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
839 );
840 });
841 search_bar.read_with(cx, |search_bar, _| {
842 assert_eq!(search_bar.active_match_index, Some(1));
843 });
844
845 search_bar.update(cx, |search_bar, cx| {
846 search_bar.select_next_match(&SelectNextMatch, cx);
847 assert_eq!(
848 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
849 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
850 );
851 });
852 search_bar.read_with(cx, |search_bar, _| {
853 assert_eq!(search_bar.active_match_index, Some(2));
854 });
855
856 search_bar.update(cx, |search_bar, cx| {
857 search_bar.select_next_match(&SelectNextMatch, cx);
858 assert_eq!(
859 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
860 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
861 );
862 });
863 search_bar.read_with(cx, |search_bar, _| {
864 assert_eq!(search_bar.active_match_index, Some(0));
865 });
866
867 search_bar.update(cx, |search_bar, cx| {
868 search_bar.select_prev_match(&SelectPrevMatch, cx);
869 assert_eq!(
870 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
871 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
872 );
873 });
874 search_bar.read_with(cx, |search_bar, _| {
875 assert_eq!(search_bar.active_match_index, Some(2));
876 });
877
878 search_bar.update(cx, |search_bar, cx| {
879 search_bar.select_prev_match(&SelectPrevMatch, cx);
880 assert_eq!(
881 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
882 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
883 );
884 });
885 search_bar.read_with(cx, |search_bar, _| {
886 assert_eq!(search_bar.active_match_index, Some(1));
887 });
888
889 search_bar.update(cx, |search_bar, cx| {
890 search_bar.select_prev_match(&SelectPrevMatch, cx);
891 assert_eq!(
892 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
893 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
894 );
895 });
896 search_bar.read_with(cx, |search_bar, _| {
897 assert_eq!(search_bar.active_match_index, Some(0));
898 });
899
900 // Park the cursor in between matches and ensure that going to the previous match selects
901 // the closest match to the left.
902 editor.update(cx, |editor, cx| {
903 editor.change_selections(None, cx, |s| {
904 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
905 });
906 });
907 search_bar.update(cx, |search_bar, cx| {
908 assert_eq!(search_bar.active_match_index, Some(1));
909 search_bar.select_prev_match(&SelectPrevMatch, cx);
910 assert_eq!(
911 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
912 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
913 );
914 });
915 search_bar.read_with(cx, |search_bar, _| {
916 assert_eq!(search_bar.active_match_index, Some(0));
917 });
918
919 // Park the cursor in between matches and ensure that going to the next match selects the
920 // closest match to the right.
921 editor.update(cx, |editor, cx| {
922 editor.change_selections(None, cx, |s| {
923 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
924 });
925 });
926 search_bar.update(cx, |search_bar, cx| {
927 assert_eq!(search_bar.active_match_index, Some(1));
928 search_bar.select_next_match(&SelectNextMatch, cx);
929 assert_eq!(
930 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
931 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
932 );
933 });
934 search_bar.read_with(cx, |search_bar, _| {
935 assert_eq!(search_bar.active_match_index, Some(1));
936 });
937
938 // Park the cursor after the last match and ensure that going to the previous match selects
939 // the last match.
940 editor.update(cx, |editor, cx| {
941 editor.change_selections(None, cx, |s| {
942 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
943 });
944 });
945 search_bar.update(cx, |search_bar, cx| {
946 assert_eq!(search_bar.active_match_index, Some(2));
947 search_bar.select_prev_match(&SelectPrevMatch, cx);
948 assert_eq!(
949 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
950 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
951 );
952 });
953 search_bar.read_with(cx, |search_bar, _| {
954 assert_eq!(search_bar.active_match_index, Some(2));
955 });
956
957 // Park the cursor after the last match and ensure that going to the next match selects the
958 // first match.
959 editor.update(cx, |editor, cx| {
960 editor.change_selections(None, cx, |s| {
961 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
962 });
963 });
964 search_bar.update(cx, |search_bar, cx| {
965 assert_eq!(search_bar.active_match_index, Some(2));
966 search_bar.select_next_match(&SelectNextMatch, cx);
967 assert_eq!(
968 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
969 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
970 );
971 });
972 search_bar.read_with(cx, |search_bar, _| {
973 assert_eq!(search_bar.active_match_index, Some(0));
974 });
975
976 // Park the cursor before the first match and ensure that going to the previous match
977 // selects the last match.
978 editor.update(cx, |editor, cx| {
979 editor.change_selections(None, cx, |s| {
980 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
981 });
982 });
983 search_bar.update(cx, |search_bar, cx| {
984 assert_eq!(search_bar.active_match_index, Some(0));
985 search_bar.select_prev_match(&SelectPrevMatch, cx);
986 assert_eq!(
987 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
988 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
989 );
990 });
991 search_bar.read_with(cx, |search_bar, _| {
992 assert_eq!(search_bar.active_match_index, Some(2));
993 });
994 }
995
996 #[gpui::test]
997 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
998 crate::project_search::tests::init_test(cx);
999
1000 let buffer_text = r#"
1001 A regular expression (shortened as regex or regexp;[1] also referred to as
1002 rational expression[2][3]) is a sequence of characters that specifies a search
1003 pattern in text. Usually such patterns are used by string-searching algorithms
1004 for "find" or "find and replace" operations on strings, or for input validation.
1005 "#
1006 .unindent();
1007 let expected_query_matches_count = buffer_text
1008 .chars()
1009 .filter(|c| c.to_ascii_lowercase() == 'a')
1010 .count();
1011 assert!(
1012 expected_query_matches_count > 1,
1013 "Should pick a query with multiple results"
1014 );
1015 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1016 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1017
1018 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1019
1020 let search_bar = cx.add_view(window_id, |cx| {
1021 let mut search_bar = BufferSearchBar::new(cx);
1022 search_bar.set_active_pane_item(Some(&editor), cx);
1023 search_bar.show(false, true, cx);
1024 search_bar
1025 });
1026
1027 search_bar.update(cx, |search_bar, cx| {
1028 search_bar.set_query("a", cx);
1029 });
1030
1031 editor.next_notification(cx).await;
1032 let initial_selections = editor.update(cx, |editor, cx| {
1033 let initial_selections = editor.selections.display_ranges(cx);
1034 assert_eq!(
1035 initial_selections.len(), 1,
1036 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1037 );
1038 initial_selections
1039 });
1040 search_bar.update(cx, |search_bar, _| {
1041 assert_eq!(search_bar.active_match_index, Some(0));
1042 });
1043
1044 search_bar.update(cx, |search_bar, cx| {
1045 search_bar.select_all_matches(&SelectAllMatches, cx);
1046 let all_selections =
1047 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1048 assert_eq!(
1049 all_selections.len(),
1050 expected_query_matches_count,
1051 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1052 );
1053 });
1054 search_bar.update(cx, |search_bar, _| {
1055 assert_eq!(
1056 search_bar.active_match_index,
1057 Some(0),
1058 "Match index should not change after selecting all matches"
1059 );
1060 });
1061
1062 search_bar.update(cx, |search_bar, cx| {
1063 search_bar.select_next_match(&SelectNextMatch, cx);
1064 let all_selections =
1065 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1066 assert_eq!(
1067 all_selections.len(),
1068 1,
1069 "On next match, should deselect items and select the next match"
1070 );
1071 assert_ne!(
1072 all_selections, initial_selections,
1073 "Next match should be different from the first selection"
1074 );
1075 });
1076 search_bar.update(cx, |search_bar, _| {
1077 assert_eq!(
1078 search_bar.active_match_index,
1079 Some(1),
1080 "Match index should be updated to the next one"
1081 );
1082 });
1083
1084 search_bar.update(cx, |search_bar, cx| {
1085 search_bar.select_all_matches(&SelectAllMatches, cx);
1086 let all_selections =
1087 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1088 assert_eq!(
1089 all_selections.len(),
1090 expected_query_matches_count,
1091 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1092 );
1093 });
1094 search_bar.update(cx, |search_bar, _| {
1095 assert_eq!(
1096 search_bar.active_match_index,
1097 Some(1),
1098 "Match index should not change after selecting all matches"
1099 );
1100 });
1101
1102 search_bar.update(cx, |search_bar, cx| {
1103 search_bar.select_prev_match(&SelectPrevMatch, cx);
1104 let all_selections =
1105 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1106 assert_eq!(
1107 all_selections.len(),
1108 1,
1109 "On previous match, should deselect items and select the previous item"
1110 );
1111 assert_eq!(
1112 all_selections, initial_selections,
1113 "Previous match should be the same as the first selection"
1114 );
1115 });
1116 search_bar.update(cx, |search_bar, _| {
1117 assert_eq!(
1118 search_bar.active_match_index,
1119 Some(0),
1120 "Match index should be updated to the previous one"
1121 );
1122 });
1123 }
1124}