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