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 {
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 match event {
454 editor::Event::BufferEdited { .. } => {
455 self.query_contains_error = false;
456 self.clear_matches(cx);
457 self.update_matches(true, cx);
458 cx.notify();
459 }
460 _ => {}
461 }
462 }
463
464 fn on_active_editor_event(
465 &mut self,
466 _: ViewHandle<Editor>,
467 event: &editor::Event,
468 cx: &mut ViewContext<Self>,
469 ) {
470 match event {
471 editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
472 editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
473 _ => {}
474 }
475 }
476
477 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
478 let mut active_editor_matches = None;
479 for (editor, ranges) in self.editors_with_matches.drain() {
480 if let Some(editor) = editor.upgrade(cx) {
481 if Some(&editor) == self.active_editor.as_ref() {
482 active_editor_matches = Some((editor.downgrade(), ranges));
483 } else {
484 editor.update(cx, |editor, cx| {
485 editor.clear_background_highlights::<Self>(cx)
486 });
487 }
488 }
489 }
490 self.editors_with_matches.extend(active_editor_matches);
491 }
492
493 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
494 let query = self.query_editor.read(cx).text(cx);
495 self.pending_search.take();
496 if let Some(editor) = self.active_editor.as_ref() {
497 if query.is_empty() {
498 self.active_match_index.take();
499 editor.update(cx, |editor, cx| {
500 editor.clear_background_highlights::<Self>(cx)
501 });
502 } else {
503 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
504 let query = if self.regex {
505 match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
506 Ok(query) => query,
507 Err(_) => {
508 self.query_contains_error = true;
509 cx.notify();
510 return;
511 }
512 }
513 } else {
514 SearchQuery::text(query, self.whole_word, self.case_sensitive)
515 };
516
517 let ranges = cx.background().spawn(async move {
518 let mut ranges = Vec::new();
519 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
520 ranges.extend(
521 query
522 .search(excerpt_buffer.as_rope())
523 .await
524 .into_iter()
525 .map(|range| {
526 buffer.anchor_after(range.start)
527 ..buffer.anchor_before(range.end)
528 }),
529 );
530 } else {
531 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
532 let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
533 let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
534 ranges.extend(query.search(&rope).await.into_iter().map(|range| {
535 let start = excerpt
536 .buffer
537 .anchor_after(excerpt_range.start + range.start);
538 let end = excerpt
539 .buffer
540 .anchor_before(excerpt_range.start + range.end);
541 buffer.anchor_in_excerpt(excerpt.id.clone(), start)
542 ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
543 }));
544 }
545 }
546 ranges
547 });
548
549 let editor = editor.downgrade();
550 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
551 let ranges = ranges.await;
552 if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
553 this.update(&mut cx, |this, cx| {
554 this.editors_with_matches
555 .insert(editor.downgrade(), ranges.clone());
556 this.update_match_index(cx);
557 if !this.dismissed {
558 editor.update(cx, |editor, cx| {
559 if select_closest_match {
560 if let Some(match_ix) = this.active_match_index {
561 editor.change_selections(
562 Some(Autoscroll::Fit),
563 cx,
564 |s| s.select_ranges([ranges[match_ix].clone()]),
565 );
566 }
567 }
568
569 editor.highlight_background::<Self>(
570 ranges,
571 |theme| theme.search.match_background,
572 cx,
573 );
574 });
575 }
576 cx.notify();
577 });
578 }
579 }));
580 }
581 }
582 }
583
584 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
585 let new_index = self.active_editor.as_ref().and_then(|editor| {
586 let ranges = self.editors_with_matches.get(&editor.downgrade())?;
587 let editor = editor.read(cx);
588 active_match_index(
589 &ranges,
590 &editor.selections.newest_anchor().head(),
591 &editor.buffer().read(cx).snapshot(cx),
592 )
593 });
594 if new_index != self.active_match_index {
595 self.active_match_index = new_index;
596 cx.notify();
597 }
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use editor::{DisplayPoint, Editor};
605 use gpui::{color::Color, test::EmptyView, TestAppContext};
606 use language::Buffer;
607 use std::sync::Arc;
608 use unindent::Unindent as _;
609
610 #[gpui::test]
611 async fn test_search_simple(cx: &mut TestAppContext) {
612 let fonts = cx.font_cache();
613 let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
614 theme.search.match_background = Color::red();
615 cx.update(|cx| {
616 let mut settings = Settings::test(cx);
617 settings.theme = Arc::new(theme);
618 cx.set_global(settings)
619 });
620
621 let buffer = cx.add_model(|cx| {
622 Buffer::new(
623 0,
624 r#"
625 A regular expression (shortened as regex or regexp;[1] also referred to as
626 rational expression[2][3]) is a sequence of characters that specifies a search
627 pattern in text. Usually such patterns are used by string-searching algorithms
628 for "find" or "find and replace" operations on strings, or for input validation.
629 "#
630 .unindent(),
631 cx,
632 )
633 });
634 let (_, root_view) = cx.add_window(|_| EmptyView);
635
636 let editor = cx.add_view(&root_view, |cx| {
637 Editor::for_buffer(buffer.clone(), None, cx)
638 });
639
640 let search_bar = cx.add_view(&root_view, |cx| {
641 let mut search_bar = BufferSearchBar::new(cx);
642 search_bar.set_active_pane_item(Some(&editor), cx);
643 search_bar.show(false, true, cx);
644 search_bar
645 });
646
647 // Search for a string that appears with different casing.
648 // By default, search is case-insensitive.
649 search_bar.update(cx, |search_bar, cx| {
650 search_bar.set_query("us", cx);
651 });
652 editor.next_notification(&cx).await;
653 editor.update(cx, |editor, cx| {
654 assert_eq!(
655 editor.all_background_highlights(cx),
656 &[
657 (
658 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
659 Color::red(),
660 ),
661 (
662 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
663 Color::red(),
664 ),
665 ]
666 );
667 });
668
669 // Switch to a case sensitive search.
670 search_bar.update(cx, |search_bar, cx| {
671 search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
672 });
673 editor.next_notification(&cx).await;
674 editor.update(cx, |editor, cx| {
675 assert_eq!(
676 editor.all_background_highlights(cx),
677 &[(
678 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
679 Color::red(),
680 )]
681 );
682 });
683
684 // Search for a string that appears both as a whole word and
685 // within other words. By default, all results are found.
686 search_bar.update(cx, |search_bar, cx| {
687 search_bar.set_query("or", cx);
688 });
689 editor.next_notification(&cx).await;
690 editor.update(cx, |editor, cx| {
691 assert_eq!(
692 editor.all_background_highlights(cx),
693 &[
694 (
695 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
696 Color::red(),
697 ),
698 (
699 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
700 Color::red(),
701 ),
702 (
703 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
704 Color::red(),
705 ),
706 (
707 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
708 Color::red(),
709 ),
710 (
711 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
712 Color::red(),
713 ),
714 (
715 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
716 Color::red(),
717 ),
718 (
719 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
720 Color::red(),
721 ),
722 ]
723 );
724 });
725
726 // Switch to a whole word search.
727 search_bar.update(cx, |search_bar, cx| {
728 search_bar.toggle_search_option(SearchOption::WholeWord, cx);
729 });
730 editor.next_notification(&cx).await;
731 editor.update(cx, |editor, cx| {
732 assert_eq!(
733 editor.all_background_highlights(cx),
734 &[
735 (
736 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
737 Color::red(),
738 ),
739 (
740 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
741 Color::red(),
742 ),
743 (
744 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
745 Color::red(),
746 ),
747 ]
748 );
749 });
750
751 editor.update(cx, |editor, cx| {
752 editor.change_selections(None, cx, |s| {
753 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
754 });
755 });
756 search_bar.update(cx, |search_bar, cx| {
757 assert_eq!(search_bar.active_match_index, Some(0));
758 search_bar.select_next_match(&SelectNextMatch, cx);
759 assert_eq!(
760 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
761 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
762 );
763 });
764 search_bar.read_with(cx, |search_bar, _| {
765 assert_eq!(search_bar.active_match_index, Some(0));
766 });
767
768 search_bar.update(cx, |search_bar, cx| {
769 search_bar.select_next_match(&SelectNextMatch, cx);
770 assert_eq!(
771 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
772 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
773 );
774 });
775 search_bar.read_with(cx, |search_bar, _| {
776 assert_eq!(search_bar.active_match_index, Some(1));
777 });
778
779 search_bar.update(cx, |search_bar, cx| {
780 search_bar.select_next_match(&SelectNextMatch, cx);
781 assert_eq!(
782 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
783 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
784 );
785 });
786 search_bar.read_with(cx, |search_bar, _| {
787 assert_eq!(search_bar.active_match_index, Some(2));
788 });
789
790 search_bar.update(cx, |search_bar, cx| {
791 search_bar.select_next_match(&SelectNextMatch, cx);
792 assert_eq!(
793 editor.update(cx, |editor, cx| editor.selections.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 search_bar.update(cx, |search_bar, cx| {
802 search_bar.select_prev_match(&SelectPrevMatch, cx);
803 assert_eq!(
804 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
805 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
806 );
807 });
808 search_bar.read_with(cx, |search_bar, _| {
809 assert_eq!(search_bar.active_match_index, Some(2));
810 });
811
812 search_bar.update(cx, |search_bar, cx| {
813 search_bar.select_prev_match(&SelectPrevMatch, 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 search_bar.update(cx, |search_bar, cx| {
824 search_bar.select_prev_match(&SelectPrevMatch, cx);
825 assert_eq!(
826 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
827 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
828 );
829 });
830 search_bar.read_with(cx, |search_bar, _| {
831 assert_eq!(search_bar.active_match_index, Some(0));
832 });
833
834 // Park the cursor in between matches and ensure that going to the previous match selects
835 // the closest match to the left.
836 editor.update(cx, |editor, cx| {
837 editor.change_selections(None, cx, |s| {
838 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
839 });
840 });
841 search_bar.update(cx, |search_bar, cx| {
842 assert_eq!(search_bar.active_match_index, Some(1));
843 search_bar.select_prev_match(&SelectPrevMatch, cx);
844 assert_eq!(
845 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
846 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
847 );
848 });
849 search_bar.read_with(cx, |search_bar, _| {
850 assert_eq!(search_bar.active_match_index, Some(0));
851 });
852
853 // Park the cursor in between matches and ensure that going to the next match selects the
854 // closest match to the right.
855 editor.update(cx, |editor, cx| {
856 editor.change_selections(None, cx, |s| {
857 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
858 });
859 });
860 search_bar.update(cx, |search_bar, cx| {
861 assert_eq!(search_bar.active_match_index, Some(1));
862 search_bar.select_next_match(&SelectNextMatch, cx);
863 assert_eq!(
864 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
865 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
866 );
867 });
868 search_bar.read_with(cx, |search_bar, _| {
869 assert_eq!(search_bar.active_match_index, Some(1));
870 });
871
872 // Park the cursor after the last match and ensure that going to the previous match selects
873 // the last match.
874 editor.update(cx, |editor, cx| {
875 editor.change_selections(None, cx, |s| {
876 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
877 });
878 });
879 search_bar.update(cx, |search_bar, cx| {
880 assert_eq!(search_bar.active_match_index, Some(2));
881 search_bar.select_prev_match(&SelectPrevMatch, cx);
882 assert_eq!(
883 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
884 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
885 );
886 });
887 search_bar.read_with(cx, |search_bar, _| {
888 assert_eq!(search_bar.active_match_index, Some(2));
889 });
890
891 // Park the cursor after the last match and ensure that going to the next match selects the
892 // first match.
893 editor.update(cx, |editor, cx| {
894 editor.change_selections(None, cx, |s| {
895 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
896 });
897 });
898 search_bar.update(cx, |search_bar, cx| {
899 assert_eq!(search_bar.active_match_index, Some(2));
900 search_bar.select_next_match(&SelectNextMatch, cx);
901 assert_eq!(
902 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
903 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
904 );
905 });
906 search_bar.read_with(cx, |search_bar, _| {
907 assert_eq!(search_bar.active_match_index, Some(0));
908 });
909
910 // Park the cursor before the first match and ensure that going to the previous match
911 // selects the last match.
912 editor.update(cx, |editor, cx| {
913 editor.change_selections(None, cx, |s| {
914 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
915 });
916 });
917 search_bar.update(cx, |search_bar, cx| {
918 assert_eq!(search_bar.active_match_index, Some(0));
919 search_bar.select_prev_match(&SelectPrevMatch, cx);
920 assert_eq!(
921 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
922 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
923 );
924 });
925 search_bar.read_with(cx, |search_bar, _| {
926 assert_eq!(search_bar.active_match_index, Some(2));
927 });
928 }
929}