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