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