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