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