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