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