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>) -> ElementBox {
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<ElementBox> {
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 ) -> ElementBox {
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(&self, theme: &theme::Search, cx: &mut ViewContext<Self>) -> ElementBox {
411 let action = Box::new(Dismiss);
412 let tooltip = "Dismiss Buffer Search";
413 let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
414
415 enum CloseButton {}
416 MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
417 let style = theme.dismiss_button.style_for(state, false);
418 Svg::new("icons/x_mark_8.svg")
419 .with_color(style.color)
420 .constrained()
421 .with_width(style.icon_width)
422 .aligned()
423 .constrained()
424 .with_width(style.button_width)
425 .contained()
426 .with_style(style.container)
427 .boxed()
428 })
429 .on_click(MouseButton::Left, {
430 let action = action.boxed_clone();
431 move |_, cx| cx.dispatch_any_action(action.boxed_clone())
432 })
433 .with_cursor_style(CursorStyle::PointingHand)
434 .with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
435 .boxed()
436 }
437
438 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
439 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
440 if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
441 return;
442 }
443 }
444 cx.propagate_action();
445 }
446
447 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
448 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
449 if !search_bar.read(cx).dismissed {
450 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
451 return;
452 }
453 }
454 cx.propagate_action();
455 }
456
457 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
458 if let Some(active_editor) = self.active_searchable_item.as_ref() {
459 cx.focus(active_editor.as_any());
460 }
461 }
462
463 fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
464 match search_option {
465 SearchOption::WholeWord => self.whole_word,
466 SearchOption::CaseSensitive => self.case_sensitive,
467 SearchOption::Regex => self.regex,
468 }
469 }
470
471 fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
472 let value = match search_option {
473 SearchOption::WholeWord => &mut self.whole_word,
474 SearchOption::CaseSensitive => &mut self.case_sensitive,
475 SearchOption::Regex => &mut self.regex,
476 };
477 *value = !*value;
478 self.update_matches(false, cx);
479 cx.notify();
480 }
481
482 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
483 self.select_match(Direction::Next, cx);
484 }
485
486 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
487 self.select_match(Direction::Prev, cx);
488 }
489
490 fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
491 if let Some(index) = self.active_match_index {
492 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
493 if let Some(matches) = self
494 .seachable_items_with_matches
495 .get(&searchable_item.downgrade())
496 {
497 let new_match_index =
498 searchable_item.match_index_for_direction(matches, index, direction, cx);
499 searchable_item.update_matches(matches, cx);
500 searchable_item.activate_match(new_match_index, matches, cx);
501 }
502 }
503 }
504 }
505
506 fn select_next_match_on_pane(
507 pane: &mut Pane,
508 action: &SelectNextMatch,
509 cx: &mut ViewContext<Pane>,
510 ) {
511 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
512 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
513 }
514 }
515
516 fn select_prev_match_on_pane(
517 pane: &mut Pane,
518 action: &SelectPrevMatch,
519 cx: &mut ViewContext<Pane>,
520 ) {
521 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
522 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
523 }
524 }
525
526 fn on_query_editor_event(
527 &mut self,
528 _: ViewHandle<Editor>,
529 event: &editor::Event,
530 cx: &mut ViewContext<Self>,
531 ) {
532 if let editor::Event::BufferEdited { .. } = event {
533 self.query_contains_error = false;
534 self.clear_matches(cx);
535 self.update_matches(true, cx);
536 cx.notify();
537 }
538 }
539
540 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
541 match event {
542 SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
543 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
544 }
545 }
546
547 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
548 let mut active_item_matches = None;
549 for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
550 if let Some(searchable_item) =
551 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
552 {
553 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
554 active_item_matches = Some((searchable_item.downgrade(), matches));
555 } else {
556 searchable_item.clear_matches(cx);
557 }
558 }
559 }
560
561 self.seachable_items_with_matches
562 .extend(active_item_matches);
563 }
564
565 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
566 let query = self.query_editor.read(cx).text(cx);
567 self.pending_search.take();
568 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
569 if query.is_empty() {
570 self.active_match_index.take();
571 active_searchable_item.clear_matches(cx);
572 } else {
573 let query = if self.regex {
574 match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
575 Ok(query) => query,
576 Err(_) => {
577 self.query_contains_error = true;
578 cx.notify();
579 return;
580 }
581 }
582 } else {
583 SearchQuery::text(query, self.whole_word, self.case_sensitive)
584 };
585
586 let matches = active_searchable_item.find_matches(query, cx);
587
588 let active_searchable_item = active_searchable_item.downgrade();
589 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
590 let matches = matches.await;
591 if let Some(this) = this.upgrade(&cx) {
592 this.update(&mut cx, |this, cx| {
593 if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(
594 active_searchable_item.as_ref(),
595 cx,
596 ) {
597 this.seachable_items_with_matches
598 .insert(active_searchable_item.downgrade(), matches);
599
600 this.update_match_index(cx);
601 if !this.dismissed {
602 let matches = this
603 .seachable_items_with_matches
604 .get(&active_searchable_item.downgrade())
605 .unwrap();
606 active_searchable_item.update_matches(matches, cx);
607 if select_closest_match {
608 if let Some(match_ix) = this.active_match_index {
609 active_searchable_item
610 .activate_match(match_ix, matches, cx);
611 }
612 }
613 }
614 cx.notify();
615 }
616 });
617 }
618 }));
619 }
620 }
621 }
622
623 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
624 let new_index = self
625 .active_searchable_item
626 .as_ref()
627 .and_then(|searchable_item| {
628 let matches = self
629 .seachable_items_with_matches
630 .get(&searchable_item.downgrade())?;
631 searchable_item.active_match_index(matches, cx)
632 });
633 if new_index != self.active_match_index {
634 self.active_match_index = new_index;
635 cx.notify();
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use editor::{DisplayPoint, Editor};
644 use gpui::{color::Color, test::EmptyView, TestAppContext};
645 use language::Buffer;
646 use std::sync::Arc;
647 use unindent::Unindent as _;
648
649 #[gpui::test]
650 async fn test_search_simple(cx: &mut TestAppContext) {
651 let fonts = cx.font_cache();
652 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
653 theme.search.match_background = Color::red();
654 cx.update(|cx| {
655 let mut settings = Settings::test(cx);
656 settings.theme = Arc::new(theme);
657 cx.set_global(settings)
658 });
659
660 let buffer = cx.add_model(|cx| {
661 Buffer::new(
662 0,
663 r#"
664 A regular expression (shortened as regex or regexp;[1] also referred to as
665 rational expression[2][3]) is a sequence of characters that specifies a search
666 pattern in text. Usually such patterns are used by string-searching algorithms
667 for "find" or "find and replace" operations on strings, or for input validation.
668 "#
669 .unindent(),
670 cx,
671 )
672 });
673 let (_, root_view) = cx.add_window(|_| EmptyView);
674
675 let editor = cx.add_view(&root_view, |cx| {
676 Editor::for_buffer(buffer.clone(), None, cx)
677 });
678
679 let search_bar = cx.add_view(&root_view, |cx| {
680 let mut search_bar = BufferSearchBar::new(cx);
681 search_bar.set_active_pane_item(Some(&editor), cx);
682 search_bar.show(false, true, cx);
683 search_bar
684 });
685
686 // Search for a string that appears with different casing.
687 // By default, search is case-insensitive.
688 search_bar.update(cx, |search_bar, cx| {
689 search_bar.set_query("us", cx);
690 });
691 editor.next_notification(cx).await;
692 editor.update(cx, |editor, cx| {
693 assert_eq!(
694 editor.all_background_highlights(cx),
695 &[
696 (
697 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
698 Color::red(),
699 ),
700 (
701 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
702 Color::red(),
703 ),
704 ]
705 );
706 });
707
708 // Switch to a case sensitive search.
709 search_bar.update(cx, |search_bar, cx| {
710 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
711 });
712 editor.next_notification(cx).await;
713 editor.update(cx, |editor, cx| {
714 assert_eq!(
715 editor.all_background_highlights(cx),
716 &[(
717 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
718 Color::red(),
719 )]
720 );
721 });
722
723 // Search for a string that appears both as a whole word and
724 // within other words. By default, all results are found.
725 search_bar.update(cx, |search_bar, cx| {
726 search_bar.set_query("or", cx);
727 });
728 editor.next_notification(cx).await;
729 editor.update(cx, |editor, cx| {
730 assert_eq!(
731 editor.all_background_highlights(cx),
732 &[
733 (
734 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
735 Color::red(),
736 ),
737 (
738 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
739 Color::red(),
740 ),
741 (
742 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
743 Color::red(),
744 ),
745 (
746 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
747 Color::red(),
748 ),
749 (
750 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
751 Color::red(),
752 ),
753 (
754 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
755 Color::red(),
756 ),
757 (
758 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
759 Color::red(),
760 ),
761 ]
762 );
763 });
764
765 // Switch to a whole word search.
766 search_bar.update(cx, |search_bar, cx| {
767 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
768 });
769 editor.next_notification(cx).await;
770 editor.update(cx, |editor, cx| {
771 assert_eq!(
772 editor.all_background_highlights(cx),
773 &[
774 (
775 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
776 Color::red(),
777 ),
778 (
779 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
780 Color::red(),
781 ),
782 (
783 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
784 Color::red(),
785 ),
786 ]
787 );
788 });
789
790 editor.update(cx, |editor, cx| {
791 editor.change_selections(None, cx, |s| {
792 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
793 });
794 });
795 search_bar.update(cx, |search_bar, cx| {
796 assert_eq!(search_bar.active_match_index, Some(0));
797 search_bar.select_next_match(&SelectNextMatch, cx);
798 assert_eq!(
799 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
800 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
801 );
802 });
803 search_bar.read_with(cx, |search_bar, _| {
804 assert_eq!(search_bar.active_match_index, Some(0));
805 });
806
807 search_bar.update(cx, |search_bar, cx| {
808 search_bar.select_next_match(&SelectNextMatch, cx);
809 assert_eq!(
810 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
811 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
812 );
813 });
814 search_bar.read_with(cx, |search_bar, _| {
815 assert_eq!(search_bar.active_match_index, Some(1));
816 });
817
818 search_bar.update(cx, |search_bar, cx| {
819 search_bar.select_next_match(&SelectNextMatch, cx);
820 assert_eq!(
821 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
822 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
823 );
824 });
825 search_bar.read_with(cx, |search_bar, _| {
826 assert_eq!(search_bar.active_match_index, Some(2));
827 });
828
829 search_bar.update(cx, |search_bar, cx| {
830 search_bar.select_next_match(&SelectNextMatch, cx);
831 assert_eq!(
832 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
833 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
834 );
835 });
836 search_bar.read_with(cx, |search_bar, _| {
837 assert_eq!(search_bar.active_match_index, Some(0));
838 });
839
840 search_bar.update(cx, |search_bar, cx| {
841 search_bar.select_prev_match(&SelectPrevMatch, cx);
842 assert_eq!(
843 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
844 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
845 );
846 });
847 search_bar.read_with(cx, |search_bar, _| {
848 assert_eq!(search_bar.active_match_index, Some(2));
849 });
850
851 search_bar.update(cx, |search_bar, cx| {
852 search_bar.select_prev_match(&SelectPrevMatch, cx);
853 assert_eq!(
854 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
855 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
856 );
857 });
858 search_bar.read_with(cx, |search_bar, _| {
859 assert_eq!(search_bar.active_match_index, Some(1));
860 });
861
862 search_bar.update(cx, |search_bar, cx| {
863 search_bar.select_prev_match(&SelectPrevMatch, cx);
864 assert_eq!(
865 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
866 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
867 );
868 });
869 search_bar.read_with(cx, |search_bar, _| {
870 assert_eq!(search_bar.active_match_index, Some(0));
871 });
872
873 // Park the cursor in between matches and ensure that going to the previous match selects
874 // the closest match to the left.
875 editor.update(cx, |editor, cx| {
876 editor.change_selections(None, cx, |s| {
877 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
878 });
879 });
880 search_bar.update(cx, |search_bar, cx| {
881 assert_eq!(search_bar.active_match_index, Some(1));
882 search_bar.select_prev_match(&SelectPrevMatch, cx);
883 assert_eq!(
884 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
885 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
886 );
887 });
888 search_bar.read_with(cx, |search_bar, _| {
889 assert_eq!(search_bar.active_match_index, Some(0));
890 });
891
892 // Park the cursor in between matches and ensure that going to the next match selects the
893 // closest match to the right.
894 editor.update(cx, |editor, cx| {
895 editor.change_selections(None, cx, |s| {
896 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
897 });
898 });
899 search_bar.update(cx, |search_bar, cx| {
900 assert_eq!(search_bar.active_match_index, Some(1));
901 search_bar.select_next_match(&SelectNextMatch, cx);
902 assert_eq!(
903 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
904 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
905 );
906 });
907 search_bar.read_with(cx, |search_bar, _| {
908 assert_eq!(search_bar.active_match_index, Some(1));
909 });
910
911 // Park the cursor after the last match and ensure that going to the previous match selects
912 // the last match.
913 editor.update(cx, |editor, cx| {
914 editor.change_selections(None, cx, |s| {
915 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
916 });
917 });
918 search_bar.update(cx, |search_bar, cx| {
919 assert_eq!(search_bar.active_match_index, Some(2));
920 search_bar.select_prev_match(&SelectPrevMatch, cx);
921 assert_eq!(
922 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
923 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
924 );
925 });
926 search_bar.read_with(cx, |search_bar, _| {
927 assert_eq!(search_bar.active_match_index, Some(2));
928 });
929
930 // Park the cursor after the last match and ensure that going to the next match selects the
931 // first match.
932 editor.update(cx, |editor, cx| {
933 editor.change_selections(None, cx, |s| {
934 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
935 });
936 });
937 search_bar.update(cx, |search_bar, cx| {
938 assert_eq!(search_bar.active_match_index, Some(2));
939 search_bar.select_next_match(&SelectNextMatch, cx);
940 assert_eq!(
941 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
942 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
943 );
944 });
945 search_bar.read_with(cx, |search_bar, _| {
946 assert_eq!(search_bar.active_match_index, Some(0));
947 });
948
949 // Park the cursor before the first match and ensure that going to the previous match
950 // selects the last match.
951 editor.update(cx, |editor, cx| {
952 editor.change_selections(None, cx, |s| {
953 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
954 });
955 });
956 search_bar.update(cx, |search_bar, cx| {
957 assert_eq!(search_bar.active_match_index, Some(0));
958 search_bar.select_prev_match(&SelectPrevMatch, cx);
959 assert_eq!(
960 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
961 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
962 );
963 });
964 search_bar.read_with(cx, |search_bar, _| {
965 assert_eq!(search_bar.active_match_index, Some(2));
966 });
967 }
968}