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