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