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