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