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