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