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