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