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