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