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