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