1use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch};
2use collections::HashMap;
3use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
4use gpui::{
5 action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
6 RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
7};
8use language::OffsetRangeExt;
9use project::search::SearchQuery;
10use std::ops::Range;
11use workspace::{ItemHandle, Pane, Settings, ToolbarItemView};
12
13action!(Deploy, bool);
14action!(Dismiss);
15action!(FocusEditor);
16action!(ToggleSearchOption, SearchOption);
17
18pub fn init(cx: &mut MutableAppContext) {
19 cx.add_bindings([
20 Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
21 Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
22 Binding::new("escape", Dismiss, Some("SearchBar")),
23 Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
24 Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
25 Binding::new(
26 "shift-enter",
27 SelectMatch(Direction::Prev),
28 Some("SearchBar"),
29 ),
30 Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
31 Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
32 ]);
33 cx.add_action(SearchBar::deploy);
34 cx.add_action(SearchBar::dismiss);
35 cx.add_action(SearchBar::focus_editor);
36 cx.add_action(SearchBar::toggle_search_option);
37 cx.add_action(SearchBar::select_match);
38 cx.add_action(SearchBar::select_match_on_pane);
39}
40
41pub struct SearchBar {
42 query_editor: ViewHandle<Editor>,
43 active_editor: Option<ViewHandle<Editor>>,
44 active_match_index: Option<usize>,
45 active_editor_subscription: Option<Subscription>,
46 editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>,
47 pending_search: Option<Task<()>>,
48 case_sensitive: bool,
49 whole_word: bool,
50 regex: bool,
51 query_contains_error: bool,
52 dismissed: bool,
53}
54
55impl Entity for SearchBar {
56 type Event = ();
57}
58
59impl View for SearchBar {
60 fn ui_name() -> &'static str {
61 "SearchBar"
62 }
63
64 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
65 cx.focus(&self.query_editor);
66 }
67
68 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
69 if self.dismissed {
70 Empty::new().boxed()
71 } else {
72 let theme = cx.global::<Settings>().theme.clone();
73 let editor_container = if self.query_contains_error {
74 theme.search.invalid_editor
75 } else {
76 theme.search.editor.container
77 };
78 Flex::row()
79 .with_child(
80 ChildView::new(&self.query_editor)
81 .contained()
82 .with_style(editor_container)
83 .aligned()
84 .constrained()
85 .with_max_width(theme.search.max_editor_width)
86 .boxed(),
87 )
88 .with_child(
89 Flex::row()
90 .with_child(self.render_search_option(
91 "Case",
92 SearchOption::CaseSensitive,
93 cx,
94 ))
95 .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
96 .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
97 .contained()
98 .with_style(theme.search.option_button_group)
99 .aligned()
100 .boxed(),
101 )
102 .with_child(
103 Flex::row()
104 .with_child(self.render_nav_button("<", Direction::Prev, cx))
105 .with_child(self.render_nav_button(">", Direction::Next, cx))
106 .aligned()
107 .boxed(),
108 )
109 .with_children(self.active_editor.as_ref().and_then(|editor| {
110 let matches = self.editors_with_matches.get(&editor.downgrade())?;
111 let message = if let Some(match_ix) = self.active_match_index {
112 format!("{}/{}", match_ix + 1, matches.len())
113 } else {
114 "No matches".to_string()
115 };
116
117 Some(
118 Label::new(message, theme.search.match_index.text.clone())
119 .contained()
120 .with_style(theme.search.match_index.container)
121 .aligned()
122 .boxed(),
123 )
124 }))
125 .named("search bar")
126 }
127 }
128}
129
130impl ToolbarItemView for SearchBar {
131 fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
132 self.active_editor_subscription.take();
133 self.active_editor.take();
134 self.pending_search.take();
135
136 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
137 if editor.read(cx).searchable() {
138 self.active_editor_subscription =
139 Some(cx.subscribe(&editor, Self::on_active_editor_event));
140 self.active_editor = Some(editor);
141 self.update_matches(false, cx);
142 self.dismissed = false;
143 return;
144 }
145 }
146
147 self.dismiss(&Dismiss, cx);
148 }
149}
150
151impl SearchBar {
152 pub fn new(cx: &mut ViewContext<Self>) -> Self {
153 let query_editor =
154 cx.add_view(|cx| Editor::auto_height(2, Some(|theme| theme.search.editor.clone()), cx));
155 cx.subscribe(&query_editor, Self::on_query_editor_event)
156 .detach();
157
158 Self {
159 query_editor,
160 active_editor: None,
161 active_editor_subscription: None,
162 active_match_index: None,
163 editors_with_matches: Default::default(),
164 case_sensitive: false,
165 whole_word: false,
166 regex: false,
167 pending_search: None,
168 query_contains_error: false,
169 dismissed: true,
170 }
171 }
172
173 fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
174 self.dismissed = true;
175 for (editor, _) in &self.editors_with_matches {
176 if let Some(editor) = editor.upgrade(cx) {
177 editor.update(cx, |editor, cx| {
178 editor.clear_background_highlights::<Self>(cx)
179 });
180 }
181 }
182 if let Some(active_editor) = self.active_editor.as_ref() {
183 cx.focus(active_editor);
184 }
185 cx.notify();
186 }
187
188 fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
189 let editor = if let Some(editor) = self.active_editor.clone() {
190 editor
191 } else {
192 return false;
193 };
194
195 let display_map = editor
196 .update(cx, |editor, cx| editor.snapshot(cx))
197 .display_snapshot;
198 let selection = editor
199 .read(cx)
200 .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
201
202 let mut text: String;
203 if selection.start == selection.end {
204 let point = selection.start.to_display_point(&display_map);
205 let range = editor::movement::surrounding_word(&display_map, point);
206 let range = range.start.to_offset(&display_map, Bias::Left)
207 ..range.end.to_offset(&display_map, Bias::Right);
208 text = display_map.buffer_snapshot.text_for_range(range).collect();
209 if text.trim().is_empty() {
210 text = String::new();
211 }
212 } else {
213 text = display_map
214 .buffer_snapshot
215 .text_for_range(selection.start..selection.end)
216 .collect();
217 }
218
219 if !text.is_empty() {
220 self.set_query(&text, cx);
221 }
222
223 if focus {
224 let query_editor = self.query_editor.clone();
225 query_editor.update(cx, |query_editor, cx| {
226 query_editor.select_all(&editor::SelectAll, cx);
227 });
228 cx.focus_self();
229 }
230
231 self.dismissed = false;
232 cx.notify();
233 true
234 }
235
236 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
237 self.query_editor.update(cx, |query_editor, cx| {
238 query_editor.buffer().update(cx, |query_buffer, cx| {
239 let len = query_buffer.read(cx).len();
240 query_buffer.edit([0..len], query, cx);
241 });
242 });
243 }
244
245 fn render_search_option(
246 &self,
247 icon: &str,
248 search_option: SearchOption,
249 cx: &mut RenderContext<Self>,
250 ) -> ElementBox {
251 let is_active = self.is_search_option_enabled(search_option);
252 MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
253 let theme = &cx.global::<Settings>().theme.search;
254 let style = match (is_active, state.hovered) {
255 (false, false) => &theme.option_button,
256 (false, true) => &theme.hovered_option_button,
257 (true, false) => &theme.active_option_button,
258 (true, true) => &theme.active_hovered_option_button,
259 };
260 Label::new(icon.to_string(), style.text.clone())
261 .contained()
262 .with_style(style.container)
263 .boxed()
264 })
265 .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
266 .with_cursor_style(CursorStyle::PointingHand)
267 .boxed()
268 }
269
270 fn render_nav_button(
271 &self,
272 icon: &str,
273 direction: Direction,
274 cx: &mut RenderContext<Self>,
275 ) -> ElementBox {
276 enum NavButton {}
277 MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
278 let theme = &cx.global::<Settings>().theme.search;
279 let style = if state.hovered {
280 &theme.hovered_option_button
281 } else {
282 &theme.option_button
283 };
284 Label::new(icon.to_string(), style.text.clone())
285 .contained()
286 .with_style(style.container)
287 .boxed()
288 })
289 .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
290 .with_cursor_style(CursorStyle::PointingHand)
291 .boxed()
292 }
293
294 fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
295 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<SearchBar>() {
296 if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
297 return;
298 }
299 }
300 cx.propagate_action();
301 }
302
303 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
304 if let Some(active_editor) = self.active_editor.as_ref() {
305 cx.focus(active_editor);
306 }
307 }
308
309 fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
310 match search_option {
311 SearchOption::WholeWord => self.whole_word,
312 SearchOption::CaseSensitive => self.case_sensitive,
313 SearchOption::Regex => self.regex,
314 }
315 }
316
317 fn toggle_search_option(
318 &mut self,
319 ToggleSearchOption(search_option): &ToggleSearchOption,
320 cx: &mut ViewContext<Self>,
321 ) {
322 let value = match search_option {
323 SearchOption::WholeWord => &mut self.whole_word,
324 SearchOption::CaseSensitive => &mut self.case_sensitive,
325 SearchOption::Regex => &mut self.regex,
326 };
327 *value = !*value;
328 self.update_matches(true, cx);
329 cx.notify();
330 }
331
332 fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
333 if let Some(index) = self.active_match_index {
334 if let Some(editor) = self.active_editor.as_ref() {
335 editor.update(cx, |editor, cx| {
336 if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
337 let new_index = match_index_for_direction(
338 ranges,
339 &editor.newest_anchor_selection().head(),
340 index,
341 direction,
342 &editor.buffer().read(cx).read(cx),
343 );
344 let range_to_select = ranges[new_index].clone();
345 editor.unfold_ranges([range_to_select.clone()], false, cx);
346 editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
347 }
348 });
349 }
350 }
351 }
352
353 fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
354 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<SearchBar>() {
355 search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
356 }
357 }
358
359 fn on_query_editor_event(
360 &mut self,
361 _: ViewHandle<Editor>,
362 event: &editor::Event,
363 cx: &mut ViewContext<Self>,
364 ) {
365 match event {
366 editor::Event::BufferEdited { .. } => {
367 self.query_contains_error = false;
368 self.clear_matches(cx);
369 self.update_matches(true, cx);
370 cx.notify();
371 }
372 _ => {}
373 }
374 }
375
376 fn on_active_editor_event(
377 &mut self,
378 _: ViewHandle<Editor>,
379 event: &editor::Event,
380 cx: &mut ViewContext<Self>,
381 ) {
382 match event {
383 editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
384 editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
385 _ => {}
386 }
387 }
388
389 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
390 let mut active_editor_matches = None;
391 for (editor, ranges) in self.editors_with_matches.drain() {
392 if let Some(editor) = editor.upgrade(cx) {
393 if Some(&editor) == self.active_editor.as_ref() {
394 active_editor_matches = Some((editor.downgrade(), ranges));
395 } else {
396 editor.update(cx, |editor, cx| {
397 editor.clear_background_highlights::<Self>(cx)
398 });
399 }
400 }
401 }
402 self.editors_with_matches.extend(active_editor_matches);
403 }
404
405 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
406 let query = self.query_editor.read(cx).text(cx);
407 self.pending_search.take();
408 if let Some(editor) = self.active_editor.as_ref() {
409 if query.is_empty() {
410 self.active_match_index.take();
411 editor.update(cx, |editor, cx| {
412 editor.clear_background_highlights::<Self>(cx)
413 });
414 } else {
415 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
416 let query = if self.regex {
417 match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
418 Ok(query) => query,
419 Err(_) => {
420 self.query_contains_error = true;
421 cx.notify();
422 return;
423 }
424 }
425 } else {
426 SearchQuery::text(query, self.whole_word, self.case_sensitive)
427 };
428
429 let ranges = cx.background().spawn(async move {
430 let mut ranges = Vec::new();
431 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
432 ranges.extend(
433 query
434 .search(excerpt_buffer.as_rope())
435 .await
436 .into_iter()
437 .map(|range| {
438 buffer.anchor_after(range.start)
439 ..buffer.anchor_before(range.end)
440 }),
441 );
442 } else {
443 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
444 let excerpt_range = excerpt.range.to_offset(&excerpt.buffer);
445 let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
446 ranges.extend(query.search(&rope).await.into_iter().map(|range| {
447 let start = excerpt
448 .buffer
449 .anchor_after(excerpt_range.start + range.start);
450 let end = excerpt
451 .buffer
452 .anchor_before(excerpt_range.start + range.end);
453 buffer.anchor_in_excerpt(excerpt.id.clone(), start)
454 ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
455 }));
456 }
457 }
458 ranges
459 });
460
461 let editor = editor.downgrade();
462 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
463 let ranges = ranges.await;
464 if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
465 this.update(&mut cx, |this, cx| {
466 this.editors_with_matches
467 .insert(editor.downgrade(), ranges.clone());
468 this.update_match_index(cx);
469 if !this.dismissed {
470 editor.update(cx, |editor, cx| {
471 if select_closest_match {
472 if let Some(match_ix) = this.active_match_index {
473 editor.select_ranges(
474 [ranges[match_ix].clone()],
475 Some(Autoscroll::Fit),
476 cx,
477 );
478 }
479 }
480
481 let theme = &cx.global::<Settings>().theme.search;
482 editor.highlight_background::<Self>(
483 ranges,
484 theme.match_background,
485 cx,
486 );
487 });
488 }
489 });
490 }
491 }));
492 }
493 }
494 }
495
496 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
497 let new_index = self.active_editor.as_ref().and_then(|editor| {
498 let ranges = self.editors_with_matches.get(&editor.downgrade())?;
499 let editor = editor.read(cx);
500 active_match_index(
501 &ranges,
502 &editor.newest_anchor_selection().head(),
503 &editor.buffer().read(cx).read(cx),
504 )
505 });
506 if new_index != self.active_match_index {
507 self.active_match_index = new_index;
508 cx.notify();
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use editor::{DisplayPoint, Editor};
517 use gpui::{color::Color, TestAppContext};
518 use language::Buffer;
519 use std::sync::Arc;
520 use unindent::Unindent as _;
521
522 #[gpui::test]
523 async fn test_search_simple(cx: &mut TestAppContext) {
524 let fonts = cx.font_cache();
525 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
526 theme.search.match_background = Color::red();
527 let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
528 cx.update(|cx| cx.set_global(settings));
529
530 let buffer = cx.add_model(|cx| {
531 Buffer::new(
532 0,
533 r#"
534 A regular expression (shortened as regex or regexp;[1] also referred to as
535 rational expression[2][3]) is a sequence of characters that specifies a search
536 pattern in text. Usually such patterns are used by string-searching algorithms
537 for "find" or "find and replace" operations on strings, or for input validation.
538 "#
539 .unindent(),
540 cx,
541 )
542 });
543 let editor = cx.add_view(Default::default(), |cx| {
544 Editor::for_buffer(buffer.clone(), None, cx)
545 });
546
547 let search_bar = cx.add_view(Default::default(), |cx| {
548 let mut search_bar = SearchBar::new(cx);
549 search_bar.set_active_pane_item(Some(&editor), cx);
550 search_bar
551 });
552
553 // Search for a string that appears with different casing.
554 // By default, search is case-insensitive.
555 search_bar.update(cx, |search_bar, cx| {
556 search_bar.set_query("us", cx);
557 });
558 editor.next_notification(&cx).await;
559 editor.update(cx, |editor, cx| {
560 assert_eq!(
561 editor.all_background_highlights(cx),
562 &[
563 (
564 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
565 Color::red(),
566 ),
567 (
568 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
569 Color::red(),
570 ),
571 ]
572 );
573 });
574
575 // Switch to a case sensitive search.
576 search_bar.update(cx, |search_bar, cx| {
577 search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx);
578 });
579 editor.next_notification(&cx).await;
580 editor.update(cx, |editor, cx| {
581 assert_eq!(
582 editor.all_background_highlights(cx),
583 &[(
584 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
585 Color::red(),
586 )]
587 );
588 });
589
590 // Search for a string that appears both as a whole word and
591 // within other words. By default, all results are found.
592 search_bar.update(cx, |search_bar, cx| {
593 search_bar.set_query("or", cx);
594 });
595 editor.next_notification(&cx).await;
596 editor.update(cx, |editor, cx| {
597 assert_eq!(
598 editor.all_background_highlights(cx),
599 &[
600 (
601 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
602 Color::red(),
603 ),
604 (
605 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
606 Color::red(),
607 ),
608 (
609 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
610 Color::red(),
611 ),
612 (
613 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
614 Color::red(),
615 ),
616 (
617 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
618 Color::red(),
619 ),
620 (
621 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
622 Color::red(),
623 ),
624 (
625 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
626 Color::red(),
627 ),
628 ]
629 );
630 });
631
632 // Switch to a whole word search.
633 search_bar.update(cx, |search_bar, cx| {
634 search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), 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 (
642 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
643 Color::red(),
644 ),
645 (
646 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
647 Color::red(),
648 ),
649 (
650 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
651 Color::red(),
652 ),
653 ]
654 );
655 });
656
657 editor.update(cx, |editor, cx| {
658 editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
659 });
660 search_bar.update(cx, |search_bar, cx| {
661 assert_eq!(search_bar.active_match_index, Some(0));
662 search_bar.select_match(&SelectMatch(Direction::Next), cx);
663 assert_eq!(
664 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
665 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
666 );
667 });
668 search_bar.read_with(cx, |search_bar, _| {
669 assert_eq!(search_bar.active_match_index, Some(0));
670 });
671
672 search_bar.update(cx, |search_bar, cx| {
673 search_bar.select_match(&SelectMatch(Direction::Next), cx);
674 assert_eq!(
675 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
676 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
677 );
678 });
679 search_bar.read_with(cx, |search_bar, _| {
680 assert_eq!(search_bar.active_match_index, Some(1));
681 });
682
683 search_bar.update(cx, |search_bar, cx| {
684 search_bar.select_match(&SelectMatch(Direction::Next), cx);
685 assert_eq!(
686 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
687 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
688 );
689 });
690 search_bar.read_with(cx, |search_bar, _| {
691 assert_eq!(search_bar.active_match_index, Some(2));
692 });
693
694 search_bar.update(cx, |search_bar, cx| {
695 search_bar.select_match(&SelectMatch(Direction::Next), cx);
696 assert_eq!(
697 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
698 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
699 );
700 });
701 search_bar.read_with(cx, |search_bar, _| {
702 assert_eq!(search_bar.active_match_index, Some(0));
703 });
704
705 search_bar.update(cx, |search_bar, cx| {
706 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
707 assert_eq!(
708 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
709 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
710 );
711 });
712 search_bar.read_with(cx, |search_bar, _| {
713 assert_eq!(search_bar.active_match_index, Some(2));
714 });
715
716 search_bar.update(cx, |search_bar, cx| {
717 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
718 assert_eq!(
719 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
720 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
721 );
722 });
723 search_bar.read_with(cx, |search_bar, _| {
724 assert_eq!(search_bar.active_match_index, Some(1));
725 });
726
727 search_bar.update(cx, |search_bar, cx| {
728 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
729 assert_eq!(
730 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
731 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
732 );
733 });
734 search_bar.read_with(cx, |search_bar, _| {
735 assert_eq!(search_bar.active_match_index, Some(0));
736 });
737
738 // Park the cursor in between matches and ensure that going to the previous match selects
739 // the closest match to the left.
740 editor.update(cx, |editor, cx| {
741 editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
742 });
743 search_bar.update(cx, |search_bar, cx| {
744 assert_eq!(search_bar.active_match_index, Some(1));
745 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
746 assert_eq!(
747 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
748 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
749 );
750 });
751 search_bar.read_with(cx, |search_bar, _| {
752 assert_eq!(search_bar.active_match_index, Some(0));
753 });
754
755 // Park the cursor in between matches and ensure that going to the next match selects the
756 // closest match to the right.
757 editor.update(cx, |editor, cx| {
758 editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
759 });
760 search_bar.update(cx, |search_bar, cx| {
761 assert_eq!(search_bar.active_match_index, Some(1));
762 search_bar.select_match(&SelectMatch(Direction::Next), cx);
763 assert_eq!(
764 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
765 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
766 );
767 });
768 search_bar.read_with(cx, |search_bar, _| {
769 assert_eq!(search_bar.active_match_index, Some(1));
770 });
771
772 // Park the cursor after the last match and ensure that going to the previous match selects
773 // the last match.
774 editor.update(cx, |editor, cx| {
775 editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
776 });
777 search_bar.update(cx, |search_bar, cx| {
778 assert_eq!(search_bar.active_match_index, Some(2));
779 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
780 assert_eq!(
781 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
782 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
783 );
784 });
785 search_bar.read_with(cx, |search_bar, _| {
786 assert_eq!(search_bar.active_match_index, Some(2));
787 });
788
789 // Park the cursor after the last match and ensure that going to the next match selects the
790 // first match.
791 editor.update(cx, |editor, cx| {
792 editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
793 });
794 search_bar.update(cx, |search_bar, cx| {
795 assert_eq!(search_bar.active_match_index, Some(2));
796 search_bar.select_match(&SelectMatch(Direction::Next), cx);
797 assert_eq!(
798 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
799 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
800 );
801 });
802 search_bar.read_with(cx, |search_bar, _| {
803 assert_eq!(search_bar.active_match_index, Some(0));
804 });
805
806 // Park the cursor before the first match and ensure that going to the previous match
807 // selects the last match.
808 editor.update(cx, |editor, cx| {
809 editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
810 });
811 search_bar.update(cx, |search_bar, cx| {
812 assert_eq!(search_bar.active_match_index, Some(0));
813 search_bar.select_match(&SelectMatch(Direction::Prev), cx);
814 assert_eq!(
815 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
816 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
817 );
818 });
819 search_bar.read_with(cx, |search_bar, _| {
820 assert_eq!(search_bar.active_match_index, Some(2));
821 });
822 }
823}