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