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 postage::watch;
10use project::search::SearchQuery;
11use std::ops::Range;
12use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
13
14action!(Deploy, bool);
15action!(Dismiss);
16action!(FocusEditor);
17action!(ToggleSearchOption, SearchOption);
18
19pub fn init(cx: &mut MutableAppContext) {
20 cx.add_bindings([
21 Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
22 Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
23 Binding::new("escape", Dismiss, Some("SearchBar")),
24 Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
25 Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
26 Binding::new(
27 "shift-enter",
28 SelectMatch(Direction::Prev),
29 Some("SearchBar"),
30 ),
31 Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
32 Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
33 ]);
34 cx.add_action(SearchBar::deploy);
35 cx.add_action(SearchBar::dismiss);
36 cx.add_action(SearchBar::focus_editor);
37 cx.add_action(SearchBar::toggle_search_option);
38 cx.add_action(SearchBar::select_match);
39 cx.add_action(SearchBar::select_match_on_pane);
40}
41
42struct SearchBar {
43 settings: watch::Receiver<Settings>,
44 query_editor: ViewHandle<Editor>,
45 active_editor: Option<ViewHandle<Editor>>,
46 active_match_index: Option<usize>,
47 active_editor_subscription: Option<Subscription>,
48 editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>,
49 pending_search: Option<Task<()>>,
50 case_sensitive: bool,
51 whole_word: bool,
52 regex: bool,
53 query_contains_error: bool,
54 dismissed: bool,
55}
56
57impl Entity for SearchBar {
58 type Event = ();
59}
60
61impl View for SearchBar {
62 fn ui_name() -> &'static str {
63 "SearchBar"
64 }
65
66 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
67 cx.focus(&self.query_editor);
68 }
69
70 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
71 let theme = &self.settings.borrow().theme;
72 let editor_container = if self.query_contains_error {
73 theme.search.invalid_editor
74 } else {
75 theme.search.editor.input.container
76 };
77 Flex::row()
78 .with_child(
79 ChildView::new(&self.query_editor)
80 .contained()
81 .with_style(editor_container)
82 .aligned()
83 .constrained()
84 .with_max_width(theme.search.editor.max_width)
85 .boxed(),
86 )
87 .with_child(
88 Flex::row()
89 .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
90 .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
91 .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
92 .contained()
93 .with_style(theme.search.option_button_group)
94 .aligned()
95 .boxed(),
96 )
97 .with_child(
98 Flex::row()
99 .with_child(self.render_nav_button("<", Direction::Prev, cx))
100 .with_child(self.render_nav_button(">", Direction::Next, cx))
101 .aligned()
102 .boxed(),
103 )
104 .with_children(self.active_editor.as_ref().and_then(|editor| {
105 let matches = self.editors_with_matches.get(&editor.downgrade())?;
106 let message = if let Some(match_ix) = self.active_match_index {
107 format!("{}/{}", match_ix + 1, matches.len())
108 } else {
109 "No matches".to_string()
110 };
111
112 Some(
113 Label::new(message, theme.search.match_index.text.clone())
114 .contained()
115 .with_style(theme.search.match_index.container)
116 .aligned()
117 .boxed(),
118 )
119 }))
120 .contained()
121 .with_style(theme.search.container)
122 .constrained()
123 .with_height(theme.workspace.toolbar.height)
124 .named("search bar")
125 }
126}
127
128impl Toolbar for SearchBar {
129 fn active_item_changed(
130 &mut self,
131 item: Option<Box<dyn ItemViewHandle>>,
132 cx: &mut ViewContext<Self>,
133 ) -> bool {
134 self.active_editor_subscription.take();
135 self.active_editor.take();
136 self.pending_search.take();
137
138 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
139 if editor.read(cx).searchable() {
140 self.active_editor_subscription =
141 Some(cx.subscribe(&editor, Self::on_active_editor_event));
142 self.active_editor = Some(editor);
143 self.update_matches(false, cx);
144 return true;
145 }
146 }
147 false
148 }
149
150 fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
151 self.dismissed = true;
152 for (editor, _) in &self.editors_with_matches {
153 if let Some(editor) = editor.upgrade(cx) {
154 editor.update(cx, |editor, cx| {
155 editor.clear_background_highlights::<Self>(cx)
156 });
157 }
158 }
159 }
160}
161
162impl SearchBar {
163 fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
164 let query_editor = cx.add_view(|cx| {
165 Editor::auto_height(
166 2,
167 settings.clone(),
168 Some(|theme| theme.search.editor.input.clone()),
169 cx,
170 )
171 });
172 cx.subscribe(&query_editor, Self::on_query_editor_event)
173 .detach();
174
175 Self {
176 query_editor,
177 active_editor: None,
178 active_editor_subscription: None,
179 active_match_index: None,
180 editors_with_matches: Default::default(),
181 case_sensitive: false,
182 whole_word: false,
183 regex: false,
184 settings,
185 pending_search: None,
186 query_contains_error: false,
187 dismissed: false,
188 }
189 }
190
191 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
192 self.query_editor.update(cx, |query_editor, cx| {
193 query_editor.buffer().update(cx, |query_buffer, cx| {
194 let len = query_buffer.read(cx).len();
195 query_buffer.edit([0..len], query, cx);
196 });
197 });
198 }
199
200 fn render_search_option(
201 &self,
202 icon: &str,
203 search_option: SearchOption,
204 cx: &mut RenderContext<Self>,
205 ) -> ElementBox {
206 let theme = &self.settings.borrow().theme.search;
207 let is_active = self.is_search_option_enabled(search_option);
208 MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, _| {
209 let style = match (is_active, state.hovered) {
210 (false, false) => &theme.option_button,
211 (false, true) => &theme.hovered_option_button,
212 (true, false) => &theme.active_option_button,
213 (true, true) => &theme.active_hovered_option_button,
214 };
215 Label::new(icon.to_string(), style.text.clone())
216 .contained()
217 .with_style(style.container)
218 .boxed()
219 })
220 .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
221 .with_cursor_style(CursorStyle::PointingHand)
222 .boxed()
223 }
224
225 fn render_nav_button(
226 &self,
227 icon: &str,
228 direction: Direction,
229 cx: &mut RenderContext<Self>,
230 ) -> ElementBox {
231 let theme = &self.settings.borrow().theme.search;
232 enum NavButton {}
233 MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, _| {
234 let style = if state.hovered {
235 &theme.hovered_option_button
236 } else {
237 &theme.option_button
238 };
239 Label::new(icon.to_string(), style.text.clone())
240 .contained()
241 .with_style(style.container)
242 .boxed()
243 })
244 .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
245 .with_cursor_style(CursorStyle::PointingHand)
246 .boxed()
247 }
248
249 fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
250 let settings = workspace.settings();
251 workspace.active_pane().update(cx, |pane, cx| {
252 pane.show_toolbar(cx, |cx| SearchBar::new(settings, cx));
253
254 if let Some(search_bar) = pane
255 .active_toolbar()
256 .and_then(|toolbar| toolbar.downcast::<Self>())
257 {
258 search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
259 let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
260 let display_map = editor
261 .update(cx, |editor, cx| editor.snapshot(cx))
262 .display_snapshot;
263 let selection = editor
264 .read(cx)
265 .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
266
267 let mut text: String;
268 if selection.start == selection.end {
269 let point = selection.start.to_display_point(&display_map);
270 let range = editor::movement::surrounding_word(&display_map, point);
271 let range = range.start.to_offset(&display_map, Bias::Left)
272 ..range.end.to_offset(&display_map, Bias::Right);
273 text = display_map.buffer_snapshot.text_for_range(range).collect();
274 if text.trim().is_empty() {
275 text = String::new();
276 }
277 } else {
278 text = display_map
279 .buffer_snapshot
280 .text_for_range(selection.start..selection.end)
281 .collect();
282 }
283
284 if !text.is_empty() {
285 search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
286 }
287
288 if *focus {
289 let query_editor = search_bar.read(cx).query_editor.clone();
290 query_editor.update(cx, |query_editor, cx| {
291 query_editor.select_all(&editor::SelectAll, cx);
292 });
293 cx.focus(&search_bar);
294 }
295 } else {
296 cx.propagate_action();
297 }
298 });
299 }
300
301 fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
302 if pane.toolbar::<SearchBar>().is_some() {
303 pane.dismiss_toolbar(cx);
304 }
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 editor.select_ranges(
349 [ranges[new_index].clone()],
350 Some(Autoscroll::Fit),
351 cx,
352 );
353 }
354 });
355 }
356 }
357 }
358
359 fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
360 if let Some(search_bar) = pane.toolbar::<SearchBar>() {
361 search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
362 }
363 }
364
365 fn on_query_editor_event(
366 &mut self,
367 _: ViewHandle<Editor>,
368 event: &editor::Event,
369 cx: &mut ViewContext<Self>,
370 ) {
371 match event {
372 editor::Event::Edited => {
373 self.query_contains_error = false;
374 self.clear_matches(cx);
375 self.update_matches(true, cx);
376 cx.notify();
377 }
378 _ => {}
379 }
380 }
381
382 fn on_active_editor_event(
383 &mut self,
384 _: ViewHandle<Editor>,
385 event: &editor::Event,
386 cx: &mut ViewContext<Self>,
387 ) {
388 match event {
389 editor::Event::Edited => self.update_matches(false, cx),
390 editor::Event::SelectionsChanged => self.update_match_index(cx),
391 _ => {}
392 }
393 }
394
395 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
396 let mut active_editor_matches = None;
397 for (editor, ranges) in self.editors_with_matches.drain() {
398 if let Some(editor) = editor.upgrade(cx) {
399 if Some(&editor) == self.active_editor.as_ref() {
400 active_editor_matches = Some((editor.downgrade(), ranges));
401 } else {
402 editor.update(cx, |editor, cx| {
403 editor.clear_background_highlights::<Self>(cx)
404 });
405 }
406 }
407 }
408 self.editors_with_matches.extend(active_editor_matches);
409 }
410
411 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
412 let query = self.query_editor.read(cx).text(cx);
413 self.pending_search.take();
414 if let Some(editor) = self.active_editor.as_ref() {
415 if query.is_empty() {
416 self.active_match_index.take();
417 editor.update(cx, |editor, cx| {
418 editor.clear_background_highlights::<Self>(cx)
419 });
420 } else {
421 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
422 let query = if self.regex {
423 match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
424 Ok(query) => query,
425 Err(_) => {
426 self.query_contains_error = true;
427 cx.notify();
428 return;
429 }
430 }
431 } else {
432 SearchQuery::text(query, self.whole_word, self.case_sensitive)
433 };
434
435 let ranges = cx.background().spawn(async move {
436 let mut ranges = Vec::new();
437 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
438 ranges.extend(
439 query
440 .search(excerpt_buffer.as_rope())
441 .await
442 .into_iter()
443 .map(|range| {
444 buffer.anchor_after(range.start)
445 ..buffer.anchor_before(range.end)
446 }),
447 );
448 } else {
449 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
450 let excerpt_range = excerpt.range.to_offset(&excerpt.buffer);
451 let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
452 ranges.extend(query.search(&rope).await.into_iter().map(|range| {
453 let start = excerpt
454 .buffer
455 .anchor_after(excerpt_range.start + range.start);
456 let end = excerpt
457 .buffer
458 .anchor_before(excerpt_range.start + range.end);
459 buffer.anchor_in_excerpt(excerpt.id.clone(), start)
460 ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
461 }));
462 }
463 }
464 ranges
465 });
466
467 let editor = editor.downgrade();
468 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
469 let ranges = ranges.await;
470 if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
471 this.update(&mut cx, |this, cx| {
472 this.editors_with_matches
473 .insert(editor.downgrade(), ranges.clone());
474 this.update_match_index(cx);
475 if !this.dismissed {
476 editor.update(cx, |editor, cx| {
477 let theme = &this.settings.borrow().theme.search;
478
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 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, MultiBuffer};
524 use gpui::{color::Color, TestAppContext};
525 use std::sync::Arc;
526 use unindent::Unindent as _;
527
528 #[gpui::test]
529 async fn test_search_simple(cx: &mut TestAppContext) {
530 let fonts = cx.font_cache();
531 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
532 theme.search.match_background = Color::red();
533 let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
534 let settings = watch::channel_with(settings).1;
535
536 let buffer = cx.update(|cx| {
537 MultiBuffer::build_simple(
538 &r#"
539 A regular expression (shortened as regex or regexp;[1] also referred to as
540 rational expression[2][3]) is a sequence of characters that specifies a search
541 pattern in text. Usually such patterns are used by string-searching algorithms
542 for "find" or "find and replace" operations on strings, or for input validation.
543 "#
544 .unindent(),
545 cx,
546 )
547 });
548 let editor = cx.add_view(Default::default(), |cx| {
549 Editor::for_buffer(buffer.clone(), None, settings.clone(), cx)
550 });
551
552 let search_bar = cx.add_view(Default::default(), |cx| {
553 let mut search_bar = SearchBar::new(settings, cx);
554 search_bar.active_item_changed(Some(Box::new(editor.clone())), 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}