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