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