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