1use crate::{
2 SearchOptions, 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>(SearchOptions::CASE_SENSITIVE, cx);
46 add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
47 add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
48}
49
50fn add_toggle_option_action<A: Action>(option: SearchOptions, 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 pending_match_direction: Option<Direction>,
69 active_searchable_item_subscription: Option<Subscription>,
70 seachable_items_with_matches:
71 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
72 pending_search: Option<Task<()>>,
73 search_options: SearchOptions,
74 default_options: SearchOptions,
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 SearchOptions::CASE_SENSITIVE,
157 cx,
158 ))
159 .with_children(self.render_search_option(
160 supported_options.word,
161 "Word",
162 SearchOptions::WHOLE_WORD,
163 cx,
164 ))
165 .with_children(self.render_search_option(
166 supported_options.regex,
167 "Regex",
168 SearchOptions::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 default_options: SearchOptions::NONE,
254 search_options: SearchOptions::NONE,
255 pending_search: None,
256 pending_match_direction: None,
257 query_contains_error: false,
258 dismissed: true,
259 }
260 }
261
262 pub fn is_dismissed(&self) -> bool {
263 self.dismissed
264 }
265
266 pub 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.as_any());
277 }
278 cx.emit(Event::UpdateLocation);
279 cx.notify();
280 }
281
282 pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
283 self.show_with_options(focus, suggest_query, self.default_options, cx)
284 }
285
286 pub fn show_with_options(
287 &mut self,
288 focus: bool,
289 suggest_query: bool,
290 search_options: SearchOptions,
291 cx: &mut ViewContext<Self>,
292 ) -> bool {
293 self.search_options = search_options;
294 let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
295 SearchableItemHandle::boxed_clone(searchable_item.as_ref())
296 } else {
297 return false;
298 };
299
300 if suggest_query {
301 let text = searchable_item.query_suggestion(cx);
302 if !text.is_empty() {
303 self.set_query(&text, cx);
304 }
305 }
306
307 if focus {
308 let query_editor = self.query_editor.clone();
309 query_editor.update(cx, |query_editor, cx| {
310 query_editor.select_all(&editor::SelectAll, cx);
311 });
312 cx.focus_self();
313 }
314
315 self.dismissed = false;
316 cx.notify();
317 cx.emit(Event::UpdateLocation);
318 true
319 }
320
321 fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
322 self.query_editor.update(cx, |query_editor, cx| {
323 query_editor.buffer().update(cx, |query_buffer, cx| {
324 let len = query_buffer.len(cx);
325 query_buffer.edit([(0..len, query)], None, cx);
326 });
327 });
328 }
329
330 fn render_search_option(
331 &self,
332 option_supported: bool,
333 icon: &'static str,
334 option: SearchOptions,
335 cx: &mut ViewContext<Self>,
336 ) -> Option<AnyElement<Self>> {
337 if !option_supported {
338 return None;
339 }
340
341 let tooltip_style = theme::current(cx).tooltip.clone();
342 let is_active = self.search_options.contains(option);
343 Some(
344 MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
345 let theme = theme::current(cx);
346 let style = theme
347 .search
348 .option_button
349 .in_state(is_active)
350 .style_for(state);
351 Label::new(icon, style.text.clone())
352 .contained()
353 .with_style(style.container)
354 })
355 .on_click(MouseButton::Left, move |_, this, cx| {
356 this.toggle_search_option(option, cx);
357 })
358 .with_cursor_style(CursorStyle::PointingHand)
359 .with_tooltip::<Self>(
360 option.bits as usize,
361 format!("Toggle {}", option.label()),
362 Some(option.to_toggle_action()),
363 tooltip_style,
364 cx,
365 )
366 .into_any(),
367 )
368 }
369
370 fn render_nav_button(
371 &self,
372 icon: &'static str,
373 direction: Direction,
374 cx: &mut ViewContext<Self>,
375 ) -> AnyElement<Self> {
376 let action: Box<dyn Action>;
377 let tooltip;
378 match direction {
379 Direction::Prev => {
380 action = Box::new(SelectPrevMatch);
381 tooltip = "Select Previous Match";
382 }
383 Direction::Next => {
384 action = Box::new(SelectNextMatch);
385 tooltip = "Select Next Match";
386 }
387 };
388 let tooltip_style = theme::current(cx).tooltip.clone();
389
390 enum NavButton {}
391 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
392 let theme = theme::current(cx);
393 let style = theme.search.option_button.inactive_state().style_for(state);
394 Label::new(icon, style.text.clone())
395 .contained()
396 .with_style(style.container)
397 })
398 .on_click(MouseButton::Left, {
399 move |_, this, cx| match direction {
400 Direction::Prev => this.select_prev_match(&Default::default(), cx),
401 Direction::Next => this.select_next_match(&Default::default(), cx),
402 }
403 })
404 .with_cursor_style(CursorStyle::PointingHand)
405 .with_tooltip::<NavButton>(
406 direction as usize,
407 tooltip.to_string(),
408 Some(action),
409 tooltip_style,
410 cx,
411 )
412 .into_any()
413 }
414
415 fn render_close_button(
416 &self,
417 theme: &theme::Search,
418 cx: &mut ViewContext<Self>,
419 ) -> AnyElement<Self> {
420 let tooltip = "Dismiss Buffer Search";
421 let tooltip_style = theme::current(cx).tooltip.clone();
422
423 enum CloseButton {}
424 MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
425 let style = theme.dismiss_button.style_for(state);
426 Svg::new("icons/x_mark_8.svg")
427 .with_color(style.color)
428 .constrained()
429 .with_width(style.icon_width)
430 .aligned()
431 .constrained()
432 .with_width(style.button_width)
433 .contained()
434 .with_style(style.container)
435 })
436 .on_click(MouseButton::Left, move |_, this, cx| {
437 this.dismiss(&Default::default(), cx)
438 })
439 .with_cursor_style(CursorStyle::PointingHand)
440 .with_tooltip::<CloseButton>(
441 0,
442 tooltip.to_string(),
443 Some(Box::new(Dismiss)),
444 tooltip_style,
445 cx,
446 )
447 .into_any()
448 }
449
450 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
451 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
452 if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
453 return;
454 }
455 }
456 cx.propagate_action();
457 }
458
459 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
460 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
461 if !search_bar.read(cx).dismissed {
462 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
463 return;
464 }
465 }
466 cx.propagate_action();
467 }
468
469 fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
470 if let Some(active_editor) = self.active_searchable_item.as_ref() {
471 cx.focus(active_editor.as_any());
472 }
473 }
474
475 fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
476 self.search_options.toggle(search_option);
477 self.default_options = self.search_options;
478
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 pub fn select_word_under_cursor(
492 &mut self,
493 direction: Direction,
494 options: SearchOptions,
495 cx: &mut ViewContext<Self>,
496 ) {
497 self.active_match_index = None;
498 self.pending_match_direction = Some(direction);
499 self.show_with_options(false, true, options, cx);
500 }
501
502 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
503 if let Some(index) = self.active_match_index {
504 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
505 if let Some(matches) = self
506 .seachable_items_with_matches
507 .get(&searchable_item.downgrade())
508 {
509 let new_match_index =
510 searchable_item.match_index_for_direction(matches, index, direction, cx);
511 searchable_item.update_matches(matches, cx);
512 searchable_item.activate_match(new_match_index, matches, cx);
513 }
514 }
515 }
516 }
517
518 fn select_next_match_on_pane(
519 pane: &mut Pane,
520 action: &SelectNextMatch,
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_next_match(action, cx));
525 }
526 }
527
528 fn select_prev_match_on_pane(
529 pane: &mut Pane,
530 action: &SelectPrevMatch,
531 cx: &mut ViewContext<Pane>,
532 ) {
533 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
534 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
535 }
536 }
537
538 fn on_query_editor_event(
539 &mut self,
540 _: ViewHandle<Editor>,
541 event: &editor::Event,
542 cx: &mut ViewContext<Self>,
543 ) {
544 if let editor::Event::BufferEdited { .. } = event {
545 self.query_contains_error = false;
546 self.clear_matches(cx);
547 self.update_matches(true, cx);
548 cx.notify();
549 }
550 }
551
552 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
553 match event {
554 SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
555 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
556 }
557 }
558
559 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
560 let mut active_item_matches = None;
561 for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
562 if let Some(searchable_item) =
563 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
564 {
565 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
566 active_item_matches = Some((searchable_item.downgrade(), matches));
567 } else {
568 searchable_item.clear_matches(cx);
569 }
570 }
571 }
572
573 self.seachable_items_with_matches
574 .extend(active_item_matches);
575 }
576
577 fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
578 let query = self.query_editor.read(cx).text(cx);
579 self.pending_search.take();
580 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
581 if query.is_empty() {
582 self.active_match_index.take();
583 self.pending_match_direction.take();
584 active_searchable_item.clear_matches(cx);
585 } else {
586 let query = if self.search_options.contains(SearchOptions::REGEX) {
587 match SearchQuery::regex(
588 query,
589 self.search_options.contains(SearchOptions::WHOLE_WORD),
590 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
591 Vec::new(),
592 Vec::new(),
593 ) {
594 Ok(query) => query,
595 Err(_) => {
596 self.query_contains_error = true;
597 cx.notify();
598 return;
599 }
600 }
601 } else {
602 SearchQuery::text(
603 query,
604 self.search_options.contains(SearchOptions::WHOLE_WORD),
605 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
606 Vec::new(),
607 Vec::new(),
608 )
609 };
610
611 let matches = active_searchable_item.find_matches(query, cx);
612
613 let active_searchable_item = active_searchable_item.downgrade();
614 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
615 let matches = matches.await;
616 this.update(&mut cx, |this, cx| {
617 if let Some(active_searchable_item) =
618 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
619 {
620 this.seachable_items_with_matches
621 .insert(active_searchable_item.downgrade(), matches);
622
623 this.update_match_index(cx);
624 if !this.dismissed {
625 let matches = this
626 .seachable_items_with_matches
627 .get(&active_searchable_item.downgrade())
628 .unwrap();
629 active_searchable_item.update_matches(matches, cx);
630 if select_closest_match {
631 if let Some(mut match_ix) = this.active_match_index {
632 if let Some(direction) = this.pending_match_direction.take()
633 {
634 match_ix += match direction {
635 Direction::Next => 1,
636 Direction::Prev => matches.len() - 1,
637 };
638 match_ix = match_ix % matches.len();
639 }
640 active_searchable_item
641 .activate_match(match_ix, matches, cx);
642 }
643 }
644 }
645 cx.notify();
646 }
647 })
648 .log_err();
649 }));
650 }
651 }
652 }
653
654 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
655 let new_index = self
656 .active_searchable_item
657 .as_ref()
658 .and_then(|searchable_item| {
659 let matches = self
660 .seachable_items_with_matches
661 .get(&searchable_item.downgrade())?;
662 searchable_item.active_match_index(matches, cx)
663 });
664 if new_index != self.active_match_index {
665 self.active_match_index = new_index;
666 cx.notify();
667 }
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use editor::{DisplayPoint, Editor};
675 use gpui::{color::Color, test::EmptyView, TestAppContext};
676 use language::Buffer;
677 use unindent::Unindent as _;
678
679 fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
680 crate::project_search::tests::init_test(cx);
681
682 let buffer = cx.add_model(|cx| {
683 Buffer::new(
684 0,
685 r#"
686 A regular expression (shortened as regex or regexp;[1] also referred to as
687 rational expression[2][3]) is a sequence of characters that specifies a search
688 pattern in text. Usually such patterns are used by string-searching algorithms
689 for "find" or "find and replace" operations on strings, or for input validation.
690 "#
691 .unindent(),
692 cx,
693 )
694 });
695 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
696
697 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
698
699 let search_bar = cx.add_view(window_id, |cx| {
700 let mut search_bar = BufferSearchBar::new(cx);
701 search_bar.set_active_pane_item(Some(&editor), cx);
702 search_bar.show(false, true, cx);
703 search_bar
704 });
705
706 (editor, search_bar)
707 }
708
709 #[gpui::test]
710 async fn test_search_simple(cx: &mut TestAppContext) {
711 let (editor, search_bar) = init_test(cx);
712
713 // Search for a string that appears with different casing.
714 // By default, search is case-insensitive.
715 search_bar.update(cx, |search_bar, cx| {
716 search_bar.set_query("us", cx);
717 });
718 editor.next_notification(cx).await;
719 editor.update(cx, |editor, cx| {
720 assert_eq!(
721 editor.all_background_highlights(cx),
722 &[
723 (
724 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
725 Color::red(),
726 ),
727 (
728 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
729 Color::red(),
730 ),
731 ]
732 );
733 });
734
735 // Switch to a case sensitive search.
736 search_bar.update(cx, |search_bar, cx| {
737 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
738 });
739 editor.next_notification(cx).await;
740 editor.update(cx, |editor, cx| {
741 assert_eq!(
742 editor.all_background_highlights(cx),
743 &[(
744 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
745 Color::red(),
746 )]
747 );
748 });
749
750 // Search for a string that appears both as a whole word and
751 // within other words. By default, all results are found.
752 search_bar.update(cx, |search_bar, cx| {
753 search_bar.set_query("or", cx);
754 });
755 editor.next_notification(cx).await;
756 editor.update(cx, |editor, cx| {
757 assert_eq!(
758 editor.all_background_highlights(cx),
759 &[
760 (
761 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
762 Color::red(),
763 ),
764 (
765 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
766 Color::red(),
767 ),
768 (
769 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
770 Color::red(),
771 ),
772 (
773 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
774 Color::red(),
775 ),
776 (
777 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
778 Color::red(),
779 ),
780 (
781 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
782 Color::red(),
783 ),
784 (
785 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
786 Color::red(),
787 ),
788 ]
789 );
790 });
791
792 // Switch to a whole word search.
793 search_bar.update(cx, |search_bar, cx| {
794 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
795 });
796 editor.next_notification(cx).await;
797 editor.update(cx, |editor, cx| {
798 assert_eq!(
799 editor.all_background_highlights(cx),
800 &[
801 (
802 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
803 Color::red(),
804 ),
805 (
806 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
807 Color::red(),
808 ),
809 (
810 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
811 Color::red(),
812 ),
813 ]
814 );
815 });
816
817 editor.update(cx, |editor, cx| {
818 editor.change_selections(None, cx, |s| {
819 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
820 });
821 });
822 search_bar.update(cx, |search_bar, cx| {
823 assert_eq!(search_bar.active_match_index, Some(0));
824 search_bar.select_next_match(&SelectNextMatch, 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 search_bar.update(cx, |search_bar, cx| {
835 search_bar.select_next_match(&SelectNextMatch, cx);
836 assert_eq!(
837 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
838 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
839 );
840 });
841 search_bar.read_with(cx, |search_bar, _| {
842 assert_eq!(search_bar.active_match_index, Some(1));
843 });
844
845 search_bar.update(cx, |search_bar, cx| {
846 search_bar.select_next_match(&SelectNextMatch, cx);
847 assert_eq!(
848 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
849 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
850 );
851 });
852 search_bar.read_with(cx, |search_bar, _| {
853 assert_eq!(search_bar.active_match_index, Some(2));
854 });
855
856 search_bar.update(cx, |search_bar, cx| {
857 search_bar.select_next_match(&SelectNextMatch, cx);
858 assert_eq!(
859 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
860 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
861 );
862 });
863 search_bar.read_with(cx, |search_bar, _| {
864 assert_eq!(search_bar.active_match_index, Some(0));
865 });
866
867 search_bar.update(cx, |search_bar, cx| {
868 search_bar.select_prev_match(&SelectPrevMatch, cx);
869 assert_eq!(
870 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
871 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
872 );
873 });
874 search_bar.read_with(cx, |search_bar, _| {
875 assert_eq!(search_bar.active_match_index, Some(2));
876 });
877
878 search_bar.update(cx, |search_bar, cx| {
879 search_bar.select_prev_match(&SelectPrevMatch, cx);
880 assert_eq!(
881 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
882 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
883 );
884 });
885 search_bar.read_with(cx, |search_bar, _| {
886 assert_eq!(search_bar.active_match_index, Some(1));
887 });
888
889 search_bar.update(cx, |search_bar, cx| {
890 search_bar.select_prev_match(&SelectPrevMatch, cx);
891 assert_eq!(
892 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
893 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
894 );
895 });
896 search_bar.read_with(cx, |search_bar, _| {
897 assert_eq!(search_bar.active_match_index, Some(0));
898 });
899
900 // Park the cursor in between matches and ensure that going to the previous match selects
901 // the closest match to the left.
902 editor.update(cx, |editor, cx| {
903 editor.change_selections(None, cx, |s| {
904 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
905 });
906 });
907 search_bar.update(cx, |search_bar, cx| {
908 assert_eq!(search_bar.active_match_index, Some(1));
909 search_bar.select_prev_match(&SelectPrevMatch, cx);
910 assert_eq!(
911 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
912 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
913 );
914 });
915 search_bar.read_with(cx, |search_bar, _| {
916 assert_eq!(search_bar.active_match_index, Some(0));
917 });
918
919 // Park the cursor in between matches and ensure that going to the next match selects the
920 // closest match to the right.
921 editor.update(cx, |editor, cx| {
922 editor.change_selections(None, cx, |s| {
923 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
924 });
925 });
926 search_bar.update(cx, |search_bar, cx| {
927 assert_eq!(search_bar.active_match_index, Some(1));
928 search_bar.select_next_match(&SelectNextMatch, cx);
929 assert_eq!(
930 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
931 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
932 );
933 });
934 search_bar.read_with(cx, |search_bar, _| {
935 assert_eq!(search_bar.active_match_index, Some(1));
936 });
937
938 // Park the cursor after the last match and ensure that going to the previous match selects
939 // the last match.
940 editor.update(cx, |editor, cx| {
941 editor.change_selections(None, cx, |s| {
942 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
943 });
944 });
945 search_bar.update(cx, |search_bar, cx| {
946 assert_eq!(search_bar.active_match_index, Some(2));
947 search_bar.select_prev_match(&SelectPrevMatch, cx);
948 assert_eq!(
949 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
950 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
951 );
952 });
953 search_bar.read_with(cx, |search_bar, _| {
954 assert_eq!(search_bar.active_match_index, Some(2));
955 });
956
957 // Park the cursor after the last match and ensure that going to the next match selects the
958 // first match.
959 editor.update(cx, |editor, cx| {
960 editor.change_selections(None, cx, |s| {
961 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
962 });
963 });
964 search_bar.update(cx, |search_bar, cx| {
965 assert_eq!(search_bar.active_match_index, Some(2));
966 search_bar.select_next_match(&SelectNextMatch, cx);
967 assert_eq!(
968 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
969 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
970 );
971 });
972 search_bar.read_with(cx, |search_bar, _| {
973 assert_eq!(search_bar.active_match_index, Some(0));
974 });
975
976 // Park the cursor before the first match and ensure that going to the previous match
977 // selects the last match.
978 editor.update(cx, |editor, cx| {
979 editor.change_selections(None, cx, |s| {
980 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
981 });
982 });
983 search_bar.update(cx, |search_bar, cx| {
984 assert_eq!(search_bar.active_match_index, Some(0));
985 search_bar.select_prev_match(&SelectPrevMatch, cx);
986 assert_eq!(
987 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
988 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
989 );
990 });
991 search_bar.read_with(cx, |search_bar, _| {
992 assert_eq!(search_bar.active_match_index, Some(2));
993 });
994 }
995
996 #[gpui::test]
997 async fn test_search_with_options(cx: &mut TestAppContext) {
998 let (editor, search_bar) = init_test(cx);
999
1000 // show with options should make current search case sensitive
1001 search_bar.update(cx, |search_bar, cx| {
1002 search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx);
1003 search_bar.set_query("us", cx);
1004 });
1005 editor.next_notification(cx).await;
1006 editor.update(cx, |editor, cx| {
1007 assert_eq!(
1008 editor.all_background_highlights(cx),
1009 &[(
1010 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1011 Color::red(),
1012 )]
1013 );
1014 });
1015
1016 // show should return to the default options (case insensitive)
1017 search_bar.update(cx, |search_bar, cx| {
1018 search_bar.show(true, true, cx);
1019 });
1020 editor.next_notification(cx).await;
1021 editor.update(cx, |editor, cx| {
1022 assert_eq!(
1023 editor.all_background_highlights(cx),
1024 &[
1025 (
1026 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1027 Color::red(),
1028 ),
1029 (
1030 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1031 Color::red(),
1032 )
1033 ]
1034 );
1035 });
1036
1037 // toggling a search option (even in show_with_options mode) should update the defaults
1038 search_bar.update(cx, |search_bar, cx| {
1039 search_bar.set_query("regex", cx);
1040 search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx);
1041 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1042 });
1043 editor.next_notification(cx).await;
1044 editor.update(cx, |editor, cx| {
1045 assert_eq!(
1046 editor.all_background_highlights(cx),
1047 &[(
1048 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1049 Color::red(),
1050 ),]
1051 );
1052 });
1053
1054 // defaults should still include whole word
1055 search_bar.update(cx, |search_bar, cx| {
1056 search_bar.show(true, true, cx);
1057 });
1058 editor.next_notification(cx).await;
1059 editor.update(cx, |editor, cx| {
1060 assert_eq!(
1061 editor.all_background_highlights(cx),
1062 &[(
1063 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1064 Color::red(),
1065 ),]
1066 );
1067 });
1068
1069 // removing whole word changes the search again
1070 search_bar.update(cx, |search_bar, cx| {
1071 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1072 });
1073 editor.next_notification(cx).await;
1074 editor.update(cx, |editor, cx| {
1075 assert_eq!(
1076 editor.all_background_highlights(cx),
1077 &[
1078 (
1079 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1080 Color::red(),
1081 ),
1082 (
1083 DisplayPoint::new(0, 44)..DisplayPoint::new(0, 49),
1084 Color::red()
1085 )
1086 ]
1087 );
1088 });
1089 }
1090}