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