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, RenderContext, Subscription, Task, View,
13 ViewContext, ViewHandle,
14};
15use project::search::SearchQuery;
16use serde::Deserialize;
17use settings::Settings;
18use std::{any::Any, sync::Arc};
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 RenderContext<Self>) -> ElementBox {
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 RenderContext<Self>,
328 ) -> Option<ElementBox> {
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 RenderContext<Self>,
368 ) -> ElementBox {
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 RenderContext<Self>,
415 ) -> ElementBox {
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 }
623 }));
624 }
625 }
626 }
627
628 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
629 let new_index = self
630 .active_searchable_item
631 .as_ref()
632 .and_then(|searchable_item| {
633 let matches = self
634 .seachable_items_with_matches
635 .get(&searchable_item.downgrade())?;
636 searchable_item.active_match_index(matches, cx)
637 });
638 if new_index != self.active_match_index {
639 self.active_match_index = new_index;
640 cx.notify();
641 }
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use editor::{DisplayPoint, Editor};
649 use gpui::{color::Color, test::EmptyView, TestAppContext};
650 use language::Buffer;
651 use std::sync::Arc;
652 use unindent::Unindent as _;
653
654 #[gpui::test]
655 async fn test_search_simple(cx: &mut TestAppContext) {
656 let fonts = cx.font_cache();
657 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
658 theme.search.match_background = Color::red();
659 cx.update(|cx| {
660 let mut settings = Settings::test(cx);
661 settings.theme = Arc::new(theme);
662 cx.set_global(settings)
663 });
664
665 let buffer = cx.add_model(|cx| {
666 Buffer::new(
667 0,
668 r#"
669 A regular expression (shortened as regex or regexp;[1] also referred to as
670 rational expression[2][3]) is a sequence of characters that specifies a search
671 pattern in text. Usually such patterns are used by string-searching algorithms
672 for "find" or "find and replace" operations on strings, or for input validation.
673 "#
674 .unindent(),
675 cx,
676 )
677 });
678 let (_, root_view) = cx.add_window(|_| EmptyView);
679
680 let editor = cx.add_view(&root_view, |cx| {
681 Editor::for_buffer(buffer.clone(), None, cx)
682 });
683
684 let search_bar = cx.add_view(&root_view, |cx| {
685 let mut search_bar = BufferSearchBar::new(cx);
686 search_bar.set_active_pane_item(Some(&editor), cx);
687 search_bar.show(false, true, cx);
688 search_bar
689 });
690
691 // Search for a string that appears with different casing.
692 // By default, search is case-insensitive.
693 search_bar.update(cx, |search_bar, cx| {
694 search_bar.set_query("us", cx);
695 });
696 editor.next_notification(cx).await;
697 editor.update(cx, |editor, cx| {
698 assert_eq!(
699 editor.all_background_highlights(cx),
700 &[
701 (
702 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
703 Color::red(),
704 ),
705 (
706 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
707 Color::red(),
708 ),
709 ]
710 );
711 });
712
713 // Switch to a case sensitive search.
714 search_bar.update(cx, |search_bar, cx| {
715 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
716 });
717 editor.next_notification(cx).await;
718 editor.update(cx, |editor, cx| {
719 assert_eq!(
720 editor.all_background_highlights(cx),
721 &[(
722 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
723 Color::red(),
724 )]
725 );
726 });
727
728 // Search for a string that appears both as a whole word and
729 // within other words. By default, all results are found.
730 search_bar.update(cx, |search_bar, cx| {
731 search_bar.set_query("or", cx);
732 });
733 editor.next_notification(cx).await;
734 editor.update(cx, |editor, cx| {
735 assert_eq!(
736 editor.all_background_highlights(cx),
737 &[
738 (
739 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
740 Color::red(),
741 ),
742 (
743 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
744 Color::red(),
745 ),
746 (
747 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
748 Color::red(),
749 ),
750 (
751 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
752 Color::red(),
753 ),
754 (
755 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
756 Color::red(),
757 ),
758 (
759 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
760 Color::red(),
761 ),
762 (
763 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
764 Color::red(),
765 ),
766 ]
767 );
768 });
769
770 // Switch to a whole word search.
771 search_bar.update(cx, |search_bar, cx| {
772 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
773 });
774 editor.next_notification(cx).await;
775 editor.update(cx, |editor, cx| {
776 assert_eq!(
777 editor.all_background_highlights(cx),
778 &[
779 (
780 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
781 Color::red(),
782 ),
783 (
784 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
785 Color::red(),
786 ),
787 (
788 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
789 Color::red(),
790 ),
791 ]
792 );
793 });
794
795 editor.update(cx, |editor, cx| {
796 editor.change_selections(None, cx, |s| {
797 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
798 });
799 });
800 search_bar.update(cx, |search_bar, cx| {
801 assert_eq!(search_bar.active_match_index, Some(0));
802 search_bar.select_next_match(&SelectNextMatch, cx);
803 assert_eq!(
804 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
805 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
806 );
807 });
808 search_bar.read_with(cx, |search_bar, _| {
809 assert_eq!(search_bar.active_match_index, Some(0));
810 });
811
812 search_bar.update(cx, |search_bar, cx| {
813 search_bar.select_next_match(&SelectNextMatch, cx);
814 assert_eq!(
815 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
816 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
817 );
818 });
819 search_bar.read_with(cx, |search_bar, _| {
820 assert_eq!(search_bar.active_match_index, Some(1));
821 });
822
823 search_bar.update(cx, |search_bar, cx| {
824 search_bar.select_next_match(&SelectNextMatch, cx);
825 assert_eq!(
826 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
827 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
828 );
829 });
830 search_bar.read_with(cx, |search_bar, _| {
831 assert_eq!(search_bar.active_match_index, Some(2));
832 });
833
834 search_bar.update(cx, |search_bar, cx| {
835 search_bar.select_next_match(&SelectNextMatch, cx);
836 assert_eq!(
837 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
838 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
839 );
840 });
841 search_bar.read_with(cx, |search_bar, _| {
842 assert_eq!(search_bar.active_match_index, Some(0));
843 });
844
845 search_bar.update(cx, |search_bar, cx| {
846 search_bar.select_prev_match(&SelectPrevMatch, cx);
847 assert_eq!(
848 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
849 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
850 );
851 });
852 search_bar.read_with(cx, |search_bar, _| {
853 assert_eq!(search_bar.active_match_index, Some(2));
854 });
855
856 search_bar.update(cx, |search_bar, cx| {
857 search_bar.select_prev_match(&SelectPrevMatch, cx);
858 assert_eq!(
859 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
860 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
861 );
862 });
863 search_bar.read_with(cx, |search_bar, _| {
864 assert_eq!(search_bar.active_match_index, Some(1));
865 });
866
867 search_bar.update(cx, |search_bar, cx| {
868 search_bar.select_prev_match(&SelectPrevMatch, cx);
869 assert_eq!(
870 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
871 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
872 );
873 });
874 search_bar.read_with(cx, |search_bar, _| {
875 assert_eq!(search_bar.active_match_index, Some(0));
876 });
877
878 // Park the cursor in between matches and ensure that going to the previous match selects
879 // the closest match to the left.
880 editor.update(cx, |editor, cx| {
881 editor.change_selections(None, cx, |s| {
882 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
883 });
884 });
885 search_bar.update(cx, |search_bar, cx| {
886 assert_eq!(search_bar.active_match_index, Some(1));
887 search_bar.select_prev_match(&SelectPrevMatch, cx);
888 assert_eq!(
889 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
890 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
891 );
892 });
893 search_bar.read_with(cx, |search_bar, _| {
894 assert_eq!(search_bar.active_match_index, Some(0));
895 });
896
897 // Park the cursor in between matches and ensure that going to the next match selects the
898 // closest match to the right.
899 editor.update(cx, |editor, cx| {
900 editor.change_selections(None, cx, |s| {
901 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
902 });
903 });
904 search_bar.update(cx, |search_bar, cx| {
905 assert_eq!(search_bar.active_match_index, Some(1));
906 search_bar.select_next_match(&SelectNextMatch, cx);
907 assert_eq!(
908 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
909 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
910 );
911 });
912 search_bar.read_with(cx, |search_bar, _| {
913 assert_eq!(search_bar.active_match_index, Some(1));
914 });
915
916 // Park the cursor after the last match and ensure that going to the previous match selects
917 // the last match.
918 editor.update(cx, |editor, cx| {
919 editor.change_selections(None, cx, |s| {
920 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
921 });
922 });
923 search_bar.update(cx, |search_bar, cx| {
924 assert_eq!(search_bar.active_match_index, Some(2));
925 search_bar.select_prev_match(&SelectPrevMatch, cx);
926 assert_eq!(
927 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
928 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
929 );
930 });
931 search_bar.read_with(cx, |search_bar, _| {
932 assert_eq!(search_bar.active_match_index, Some(2));
933 });
934
935 // Park the cursor after the last match and ensure that going to the next match selects the
936 // first match.
937 editor.update(cx, |editor, cx| {
938 editor.change_selections(None, cx, |s| {
939 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
940 });
941 });
942 search_bar.update(cx, |search_bar, cx| {
943 assert_eq!(search_bar.active_match_index, Some(2));
944 search_bar.select_next_match(&SelectNextMatch, cx);
945 assert_eq!(
946 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
947 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
948 );
949 });
950 search_bar.read_with(cx, |search_bar, _| {
951 assert_eq!(search_bar.active_match_index, Some(0));
952 });
953
954 // Park the cursor before the first match and ensure that going to the previous match
955 // selects the last match.
956 editor.update(cx, |editor, cx| {
957 editor.change_selections(None, cx, |s| {
958 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
959 });
960 });
961 search_bar.update(cx, |search_bar, cx| {
962 assert_eq!(search_bar.active_match_index, Some(0));
963 search_bar.select_prev_match(&SelectPrevMatch, cx);
964 assert_eq!(
965 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
966 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
967 );
968 });
969 search_bar.read_with(cx, |search_bar, _| {
970 assert_eq!(search_bar.active_match_index, Some(2));
971 });
972 }
973}