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