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