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