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