1use crate::{
2 history::SearchHistory, mode::SearchMode, search_bar::render_search_mode_button,
3 NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch,
4 SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
5};
6use collections::HashMap;
7use editor::Editor;
8use futures::channel::oneshot;
9use gpui::{
10 actions,
11 elements::*,
12 impl_actions,
13 platform::{CursorStyle, MouseButton},
14 Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
15 WindowContext,
16};
17use project::search::SearchQuery;
18use serde::Deserialize;
19use std::{any::Any, sync::Arc};
20use util::ResultExt;
21use workspace::{
22 item::ItemHandle,
23 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
24 Pane, ToolbarItemLocation, ToolbarItemView,
25};
26
27#[derive(Clone, Deserialize, PartialEq)]
28pub struct Deploy {
29 pub focus: bool,
30}
31
32actions!(buffer_search, [Dismiss, FocusEditor]);
33impl_actions!(buffer_search, [Deploy]);
34
35pub enum Event {
36 UpdateLocation,
37}
38
39pub fn init(cx: &mut AppContext) {
40 cx.add_action(BufferSearchBar::deploy);
41 cx.add_action(BufferSearchBar::dismiss);
42 cx.add_action(BufferSearchBar::focus_editor);
43 cx.add_action(BufferSearchBar::select_next_match);
44 cx.add_action(BufferSearchBar::select_prev_match);
45 cx.add_action(BufferSearchBar::select_all_matches);
46 cx.add_action(BufferSearchBar::select_next_match_on_pane);
47 cx.add_action(BufferSearchBar::select_prev_match_on_pane);
48 cx.add_action(BufferSearchBar::select_all_matches_on_pane);
49 cx.add_action(BufferSearchBar::handle_editor_cancel);
50 cx.add_action(BufferSearchBar::next_history_query);
51 cx.add_action(BufferSearchBar::previous_history_query);
52 add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
53 add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
54}
55
56fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
57 cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
58 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
59 search_bar.update(cx, |search_bar, cx| {
60 if search_bar.show(cx) {
61 search_bar.toggle_search_option(option, cx);
62 }
63 });
64 }
65 cx.propagate_action();
66 });
67}
68
69pub struct BufferSearchBar {
70 query_editor: ViewHandle<Editor>,
71 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
72 active_match_index: Option<usize>,
73 active_searchable_item_subscription: Option<Subscription>,
74 searchable_items_with_matches:
75 HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
76 pending_search: Option<Task<()>>,
77 search_options: SearchOptions,
78 default_options: SearchOptions,
79 query_contains_error: bool,
80 dismissed: bool,
81 search_history: SearchHistory,
82 current_mode: SearchMode,
83}
84
85impl Entity for BufferSearchBar {
86 type Event = Event;
87}
88
89impl View for BufferSearchBar {
90 fn ui_name() -> &'static str {
91 "BufferSearchBar"
92 }
93
94 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
95 if cx.is_self_focused() {
96 cx.focus(&self.query_editor);
97 }
98 }
99
100 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
101 let theme = theme::current(cx).clone();
102 let editor_container = if self.query_contains_error {
103 theme.search.invalid_editor
104 } else {
105 theme.search.editor.input.container
106 };
107 let supported_options = self
108 .active_searchable_item
109 .as_ref()
110 .map(|active_searchable_item| active_searchable_item.supported_options())
111 .unwrap_or_default();
112
113 let previous_query_keystrokes =
114 cx.binding_for_action(&PreviousHistoryQuery {})
115 .map(|binding| {
116 binding
117 .keystrokes()
118 .iter()
119 .map(|k| k.to_string())
120 .collect::<Vec<_>>()
121 });
122 let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
123 binding
124 .keystrokes()
125 .iter()
126 .map(|k| k.to_string())
127 .collect::<Vec<_>>()
128 });
129 let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
130 (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
131 format!(
132 "Search ({}/{} for previous/next query)",
133 previous_query_keystrokes.join(" "),
134 next_query_keystrokes.join(" ")
135 )
136 }
137 (None, Some(next_query_keystrokes)) => {
138 format!(
139 "Search ({} for next query)",
140 next_query_keystrokes.join(" ")
141 )
142 }
143 (Some(previous_query_keystrokes), None) => {
144 format!(
145 "Search ({} for previous query)",
146 previous_query_keystrokes.join(" ")
147 )
148 }
149 (None, None) => String::new(),
150 };
151 self.query_editor.update(cx, |editor, cx| {
152 editor.set_placeholder_text(new_placeholder_text, cx);
153 });
154 let search_button_for_mode = |mode, cx: &mut ViewContext<BufferSearchBar>| {
155 let is_active = self.current_mode == mode;
156
157 render_search_mode_button(
158 mode,
159 is_active,
160 move |_, this, cx| {
161 this.activate_search_mode(mode, cx);
162 },
163 cx,
164 )
165 };
166 let render_search_option =
167 |options: bool, icon, option, cx: &mut ViewContext<BufferSearchBar>| {
168 options.then(|| {
169 let is_active = self.search_options.contains(option);
170 crate::search_bar::render_option_button_icon::<Self>(
171 is_active,
172 icon,
173 option,
174 move |_, this, cx| {
175 this.toggle_search_option(option, cx);
176 },
177 cx,
178 )
179 })
180 };
181 Flex::row()
182 .with_child(
183 Flex::column()
184 .with_child(
185 Flex::row()
186 .align_children_center()
187 .with_child(
188 Flex::row()
189 .with_child(self.render_nav_button("<", Direction::Prev, cx))
190 .with_child(self.render_nav_button(">", Direction::Next, cx))
191 .aligned(),
192 )
193 .with_children(self.active_searchable_item.as_ref().and_then(
194 |searchable_item| {
195 let matches = self
196 .searchable_items_with_matches
197 .get(&searchable_item.downgrade())?;
198 let message = if let Some(match_ix) = self.active_match_index {
199 format!("{}/{}", match_ix + 1, matches.len())
200 } else {
201 "No matches".to_string()
202 };
203
204 Some(
205 Label::new(message, theme.search.match_index.text.clone())
206 .contained()
207 .with_style(theme.search.match_index.container)
208 .aligned(),
209 )
210 },
211 ))
212 .aligned()
213 .left()
214 .top()
215 .flex(1., true)
216 .constrained(),
217 )
218 .contained(),
219 )
220 .with_child(
221 Flex::column()
222 .align_children_center()
223 .with_child(
224 Flex::row()
225 .with_child(
226 Flex::row()
227 .with_child(
228 ChildView::new(&self.query_editor, cx)
229 .aligned()
230 .left()
231 .flex(1., true),
232 )
233 .with_children(render_search_option(
234 supported_options.case,
235 "icons/case_insensitive_12.svg",
236 SearchOptions::CASE_SENSITIVE,
237 cx,
238 ))
239 .with_children(render_search_option(
240 supported_options.word,
241 "icons/word_search_12.svg",
242 SearchOptions::WHOLE_WORD,
243 cx,
244 ))
245 .contained()
246 .with_style(editor_container)
247 .aligned()
248 .top()
249 .constrained()
250 .with_min_width(theme.search.editor.min_width)
251 .with_max_width(theme.search.editor.max_width)
252 .flex(1., false),
253 )
254 .with_child(
255 Flex::row()
256 .with_child(self.render_action_button("Select All", cx))
257 .aligned(),
258 )
259 .flex(1., false),
260 )
261 .contained()
262 .aligned()
263 .top()
264 .flex(1., false),
265 )
266 .with_child(
267 Flex::column().with_child(
268 Flex::row()
269 .align_children_center()
270 .with_child(search_button_for_mode(SearchMode::Text, cx))
271 .with_child(search_button_for_mode(SearchMode::Regex, cx))
272 .with_child(super::search_bar::render_close_button(
273 &theme.search,
274 cx,
275 |_, this, cx| this.dismiss(&Default::default(), cx),
276 Some(Box::new(Dismiss)),
277 ))
278 .contained()
279 .aligned()
280 .right()
281 .top()
282 .flex(1., true),
283 ),
284 )
285 .contained()
286 .with_style(theme.search.container)
287 .flex_float()
288 .into_any_named("search bar")
289 }
290}
291
292impl ToolbarItemView for BufferSearchBar {
293 fn set_active_pane_item(
294 &mut self,
295 item: Option<&dyn ItemHandle>,
296 cx: &mut ViewContext<Self>,
297 ) -> ToolbarItemLocation {
298 cx.notify();
299 self.active_searchable_item_subscription.take();
300 self.active_searchable_item.take();
301 self.pending_search.take();
302
303 if let Some(searchable_item_handle) =
304 item.and_then(|item| item.to_searchable_item_handle(cx))
305 {
306 let this = cx.weak_handle();
307 self.active_searchable_item_subscription =
308 Some(searchable_item_handle.subscribe_to_search_events(
309 cx,
310 Box::new(move |search_event, cx| {
311 if let Some(this) = this.upgrade(cx) {
312 this.update(cx, |this, cx| {
313 this.on_active_searchable_item_event(search_event, cx)
314 });
315 }
316 }),
317 ));
318
319 self.active_searchable_item = Some(searchable_item_handle);
320 let _ = self.update_matches(cx);
321 if !self.dismissed {
322 return ToolbarItemLocation::Secondary;
323 }
324 }
325
326 ToolbarItemLocation::Hidden
327 }
328
329 fn location_for_event(
330 &self,
331 _: &Self::Event,
332 _: ToolbarItemLocation,
333 _: &AppContext,
334 ) -> ToolbarItemLocation {
335 if self.active_searchable_item.is_some() && !self.dismissed {
336 ToolbarItemLocation::Secondary
337 } else {
338 ToolbarItemLocation::Hidden
339 }
340 }
341}
342
343impl BufferSearchBar {
344 pub fn new(cx: &mut ViewContext<Self>) -> Self {
345 let query_editor = cx.add_view(|cx| {
346 Editor::auto_height(
347 2,
348 Some(Arc::new(|theme| theme.search.editor.input.clone())),
349 cx,
350 )
351 });
352 cx.subscribe(&query_editor, Self::on_query_editor_event)
353 .detach();
354
355 Self {
356 query_editor,
357 active_searchable_item: None,
358 active_searchable_item_subscription: None,
359 active_match_index: None,
360 searchable_items_with_matches: Default::default(),
361 default_options: SearchOptions::NONE,
362 search_options: SearchOptions::NONE,
363 pending_search: None,
364 query_contains_error: false,
365 dismissed: true,
366 search_history: SearchHistory::default(),
367 current_mode: SearchMode::default(),
368 }
369 }
370
371 pub fn is_dismissed(&self) -> bool {
372 self.dismissed
373 }
374
375 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
376 self.dismissed = true;
377 for searchable_item in self.searchable_items_with_matches.keys() {
378 if let Some(searchable_item) =
379 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
380 {
381 searchable_item.clear_matches(cx);
382 }
383 }
384 if let Some(active_editor) = self.active_searchable_item.as_ref() {
385 cx.focus(active_editor.as_any());
386 }
387 cx.emit(Event::UpdateLocation);
388 cx.notify();
389 }
390
391 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
392 if self.active_searchable_item.is_none() {
393 return false;
394 }
395 self.dismissed = false;
396 cx.notify();
397 cx.emit(Event::UpdateLocation);
398 true
399 }
400
401 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
402 let search = self
403 .query_suggestion(cx)
404 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
405
406 if let Some(search) = search {
407 cx.spawn(|this, mut cx| async move {
408 search.await?;
409 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
410 })
411 .detach_and_log_err(cx);
412 }
413 }
414
415 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
416 if let Some(match_ix) = self.active_match_index {
417 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
418 if let Some(matches) = self
419 .searchable_items_with_matches
420 .get(&active_searchable_item.downgrade())
421 {
422 active_searchable_item.activate_match(match_ix, matches, cx)
423 }
424 }
425 }
426 }
427
428 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
429 self.query_editor.update(cx, |query_editor, cx| {
430 query_editor.select_all(&Default::default(), cx);
431 });
432 }
433
434 pub fn query(&self, cx: &WindowContext) -> String {
435 self.query_editor.read(cx).text(cx)
436 }
437
438 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
439 self.active_searchable_item
440 .as_ref()
441 .map(|searchable_item| searchable_item.query_suggestion(cx))
442 }
443
444 pub fn search(
445 &mut self,
446 query: &str,
447 options: Option<SearchOptions>,
448 cx: &mut ViewContext<Self>,
449 ) -> oneshot::Receiver<()> {
450 let options = options.unwrap_or(self.default_options);
451 if query != self.query(cx) || self.search_options != options {
452 self.query_editor.update(cx, |query_editor, cx| {
453 query_editor.buffer().update(cx, |query_buffer, cx| {
454 let len = query_buffer.len(cx);
455 query_buffer.edit([(0..len, query)], None, cx);
456 });
457 });
458 self.search_options = options;
459 self.query_contains_error = false;
460 self.clear_matches(cx);
461 cx.notify();
462 }
463 self.update_matches(cx)
464 }
465
466 fn render_search_option(
467 &self,
468 option_supported: bool,
469 icon: &'static str,
470 option: SearchOptions,
471 cx: &mut ViewContext<Self>,
472 ) -> Option<AnyElement<Self>> {
473 if !option_supported {
474 return None;
475 }
476
477 let tooltip_style = theme::current(cx).tooltip.clone();
478 let is_active = self.search_options.contains(option);
479 Some(
480 MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
481 let theme = theme::current(cx);
482 let style = theme
483 .search
484 .option_button
485 .in_state(is_active)
486 .style_for(state);
487 Label::new(icon, style.text.clone())
488 .contained()
489 .with_style(style.container)
490 })
491 .on_click(MouseButton::Left, move |_, this, cx| {
492 this.toggle_search_option(option, cx);
493 })
494 .with_cursor_style(CursorStyle::PointingHand)
495 .with_tooltip::<Self>(
496 option.bits as usize,
497 format!("Toggle {}", option.label()),
498 Some(option.to_toggle_action()),
499 tooltip_style,
500 cx,
501 )
502 .into_any(),
503 )
504 }
505
506 fn render_nav_button(
507 &self,
508 icon: &'static str,
509 direction: Direction,
510 cx: &mut ViewContext<Self>,
511 ) -> AnyElement<Self> {
512 let action: Box<dyn Action>;
513 let tooltip;
514 match direction {
515 Direction::Prev => {
516 action = Box::new(SelectPrevMatch);
517 tooltip = "Select Previous Match";
518 }
519 Direction::Next => {
520 action = Box::new(SelectNextMatch);
521 tooltip = "Select Next Match";
522 }
523 };
524 let tooltip_style = theme::current(cx).tooltip.clone();
525
526 enum NavButton {}
527 MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
528 let theme = theme::current(cx);
529 let style = theme.search.option_button.inactive_state().style_for(state);
530 Label::new(icon, style.text.clone())
531 .contained()
532 .with_style(style.container)
533 })
534 .on_click(MouseButton::Left, {
535 move |_, this, cx| match direction {
536 Direction::Prev => this.select_prev_match(&Default::default(), cx),
537 Direction::Next => this.select_next_match(&Default::default(), cx),
538 }
539 })
540 .with_cursor_style(CursorStyle::PointingHand)
541 .with_tooltip::<NavButton>(
542 direction as usize,
543 tooltip.to_string(),
544 Some(action),
545 tooltip_style,
546 cx,
547 )
548 .into_any()
549 }
550
551 fn render_action_button(
552 &self,
553 icon: &'static str,
554 cx: &mut ViewContext<Self>,
555 ) -> AnyElement<Self> {
556 let tooltip = "Select All Matches";
557 let tooltip_style = theme::current(cx).tooltip.clone();
558 let action_type_id = 0_usize;
559
560 enum ActionButton {}
561 MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
562 let theme = theme::current(cx);
563 let style = theme.search.action_button.style_for(state);
564 Label::new(icon, style.text.clone())
565 .contained()
566 .with_style(style.container)
567 })
568 .on_click(MouseButton::Left, move |_, this, cx| {
569 this.select_all_matches(&SelectAllMatches, cx)
570 })
571 .with_cursor_style(CursorStyle::PointingHand)
572 .with_tooltip::<ActionButton>(
573 action_type_id,
574 tooltip.to_string(),
575 Some(Box::new(SelectAllMatches)),
576 tooltip_style,
577 cx,
578 )
579 .into_any()
580 }
581 pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
582 assert_ne!(
583 mode,
584 SearchMode::Semantic,
585 "Semantic search is not supported in buffer search"
586 );
587 if mode == self.current_mode {
588 return;
589 }
590 self.current_mode = mode;
591 let _ = self.update_matches(cx);
592 cx.notify();
593 }
594 fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
595 let mut propagate_action = true;
596 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
597 search_bar.update(cx, |search_bar, cx| {
598 if search_bar.show(cx) {
599 search_bar.search_suggested(cx);
600 if action.focus {
601 search_bar.select_query(cx);
602 cx.focus_self();
603 }
604 propagate_action = false;
605 }
606 });
607 }
608
609 if propagate_action {
610 cx.propagate_action();
611 }
612 }
613
614 fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
615 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
616 if !search_bar.read(cx).dismissed {
617 search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
618 return;
619 }
620 }
621 cx.propagate_action();
622 }
623
624 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
625 if let Some(active_editor) = self.active_searchable_item.as_ref() {
626 cx.focus(active_editor.as_any());
627 }
628 }
629
630 fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
631 self.search_options.toggle(search_option);
632 self.default_options = self.search_options;
633 let _ = self.update_matches(cx);
634 cx.notify();
635 }
636
637 pub fn set_search_options(
638 &mut self,
639 search_options: SearchOptions,
640 cx: &mut ViewContext<Self>,
641 ) {
642 self.search_options = search_options;
643 cx.notify();
644 }
645
646 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
647 self.select_match(Direction::Next, 1, cx);
648 }
649
650 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
651 self.select_match(Direction::Prev, 1, cx);
652 }
653
654 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
655 if !self.dismissed && self.active_match_index.is_some() {
656 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
657 if let Some(matches) = self
658 .searchable_items_with_matches
659 .get(&searchable_item.downgrade())
660 {
661 searchable_item.select_matches(matches, cx);
662 self.focus_editor(&FocusEditor, cx);
663 }
664 }
665 }
666 }
667
668 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
669 if let Some(index) = self.active_match_index {
670 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
671 if let Some(matches) = self
672 .searchable_items_with_matches
673 .get(&searchable_item.downgrade())
674 {
675 let new_match_index = searchable_item
676 .match_index_for_direction(matches, index, direction, count, cx);
677 searchable_item.update_matches(matches, cx);
678 searchable_item.activate_match(new_match_index, matches, cx);
679 }
680 }
681 }
682 }
683
684 fn select_next_match_on_pane(
685 pane: &mut Pane,
686 action: &SelectNextMatch,
687 cx: &mut ViewContext<Pane>,
688 ) {
689 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
690 search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
691 }
692 }
693
694 fn select_prev_match_on_pane(
695 pane: &mut Pane,
696 action: &SelectPrevMatch,
697 cx: &mut ViewContext<Pane>,
698 ) {
699 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
700 search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
701 }
702 }
703
704 fn select_all_matches_on_pane(
705 pane: &mut Pane,
706 action: &SelectAllMatches,
707 cx: &mut ViewContext<Pane>,
708 ) {
709 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
710 search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
711 }
712 }
713
714 fn on_query_editor_event(
715 &mut self,
716 _: ViewHandle<Editor>,
717 event: &editor::Event,
718 cx: &mut ViewContext<Self>,
719 ) {
720 if let editor::Event::Edited { .. } = event {
721 self.query_contains_error = false;
722 self.clear_matches(cx);
723 let search = self.update_matches(cx);
724 cx.spawn(|this, mut cx| async move {
725 search.await?;
726 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
727 })
728 .detach_and_log_err(cx);
729 }
730 }
731
732 fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
733 match event {
734 SearchEvent::MatchesInvalidated => {
735 let _ = self.update_matches(cx);
736 }
737 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
738 }
739 }
740
741 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
742 let mut active_item_matches = None;
743 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
744 if let Some(searchable_item) =
745 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
746 {
747 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
748 active_item_matches = Some((searchable_item.downgrade(), matches));
749 } else {
750 searchable_item.clear_matches(cx);
751 }
752 }
753 }
754
755 self.searchable_items_with_matches
756 .extend(active_item_matches);
757 }
758
759 fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
760 let (done_tx, done_rx) = oneshot::channel();
761 let query = self.query(cx);
762 self.pending_search.take();
763 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
764 if query.is_empty() {
765 self.active_match_index.take();
766 active_searchable_item.clear_matches(cx);
767 let _ = done_tx.send(());
768 } else {
769 let query = if self.current_mode == SearchMode::Regex {
770 match SearchQuery::regex(
771 query,
772 self.search_options.contains(SearchOptions::WHOLE_WORD),
773 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
774 Vec::new(),
775 Vec::new(),
776 ) {
777 Ok(query) => query,
778 Err(_) => {
779 self.query_contains_error = true;
780 cx.notify();
781 return done_rx;
782 }
783 }
784 } else {
785 SearchQuery::text(
786 query,
787 self.search_options.contains(SearchOptions::WHOLE_WORD),
788 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
789 Vec::new(),
790 Vec::new(),
791 )
792 };
793
794 let query_text = query.as_str().to_string();
795 let matches = active_searchable_item.find_matches(query, cx);
796
797 let active_searchable_item = active_searchable_item.downgrade();
798 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
799 let matches = matches.await;
800 this.update(&mut cx, |this, cx| {
801 if let Some(active_searchable_item) =
802 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
803 {
804 this.searchable_items_with_matches
805 .insert(active_searchable_item.downgrade(), matches);
806
807 this.update_match_index(cx);
808 this.search_history.add(query_text);
809 if !this.dismissed {
810 let matches = this
811 .searchable_items_with_matches
812 .get(&active_searchable_item.downgrade())
813 .unwrap();
814 active_searchable_item.update_matches(matches, cx);
815 let _ = done_tx.send(());
816 }
817 cx.notify();
818 }
819 })
820 .log_err();
821 }));
822 }
823 }
824 done_rx
825 }
826
827 fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
828 let new_index = self
829 .active_searchable_item
830 .as_ref()
831 .and_then(|searchable_item| {
832 let matches = self
833 .searchable_items_with_matches
834 .get(&searchable_item.downgrade())?;
835 searchable_item.active_match_index(matches, cx)
836 });
837 if new_index != self.active_match_index {
838 self.active_match_index = new_index;
839 cx.notify();
840 }
841 }
842
843 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
844 if let Some(new_query) = self.search_history.next().map(str::to_string) {
845 let _ = self.search(&new_query, Some(self.search_options), cx);
846 } else {
847 self.search_history.reset_selection();
848 let _ = self.search("", Some(self.search_options), cx);
849 }
850 }
851
852 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
853 if self.query(cx).is_empty() {
854 if let Some(new_query) = self.search_history.current().map(str::to_string) {
855 let _ = self.search(&new_query, Some(self.search_options), cx);
856 return;
857 }
858 }
859
860 if let Some(new_query) = self.search_history.previous().map(str::to_string) {
861 let _ = self.search(&new_query, Some(self.search_options), cx);
862 }
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use editor::{DisplayPoint, Editor};
870 use gpui::{color::Color, test::EmptyView, TestAppContext};
871 use language::Buffer;
872 use unindent::Unindent as _;
873
874 fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
875 crate::project_search::tests::init_test(cx);
876
877 let buffer = cx.add_model(|cx| {
878 Buffer::new(
879 0,
880 r#"
881 A regular expression (shortened as regex or regexp;[1] also referred to as
882 rational expression[2][3]) is a sequence of characters that specifies a search
883 pattern in text. Usually such patterns are used by string-searching algorithms
884 for "find" or "find and replace" operations on strings, or for input validation.
885 "#
886 .unindent(),
887 cx,
888 )
889 });
890 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
891
892 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
893
894 let search_bar = cx.add_view(window_id, |cx| {
895 let mut search_bar = BufferSearchBar::new(cx);
896 search_bar.set_active_pane_item(Some(&editor), cx);
897 search_bar.show(cx);
898 search_bar
899 });
900
901 (editor, search_bar)
902 }
903
904 #[gpui::test]
905 async fn test_search_simple(cx: &mut TestAppContext) {
906 let (editor, search_bar) = init_test(cx);
907
908 // Search for a string that appears with different casing.
909 // By default, search is case-insensitive.
910 search_bar
911 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
912 .await
913 .unwrap();
914 editor.update(cx, |editor, cx| {
915 assert_eq!(
916 editor.all_background_highlights(cx),
917 &[
918 (
919 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
920 Color::red(),
921 ),
922 (
923 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
924 Color::red(),
925 ),
926 ]
927 );
928 });
929
930 // Switch to a case sensitive search.
931 search_bar.update(cx, |search_bar, cx| {
932 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
933 });
934 editor.next_notification(cx).await;
935 editor.update(cx, |editor, cx| {
936 assert_eq!(
937 editor.all_background_highlights(cx),
938 &[(
939 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
940 Color::red(),
941 )]
942 );
943 });
944
945 // Search for a string that appears both as a whole word and
946 // within other words. By default, all results are found.
947 search_bar
948 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
949 .await
950 .unwrap();
951 editor.update(cx, |editor, cx| {
952 assert_eq!(
953 editor.all_background_highlights(cx),
954 &[
955 (
956 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
957 Color::red(),
958 ),
959 (
960 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
961 Color::red(),
962 ),
963 (
964 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
965 Color::red(),
966 ),
967 (
968 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
969 Color::red(),
970 ),
971 (
972 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
973 Color::red(),
974 ),
975 (
976 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
977 Color::red(),
978 ),
979 (
980 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
981 Color::red(),
982 ),
983 ]
984 );
985 });
986
987 // Switch to a whole word search.
988 search_bar.update(cx, |search_bar, cx| {
989 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
990 });
991 editor.next_notification(cx).await;
992 editor.update(cx, |editor, cx| {
993 assert_eq!(
994 editor.all_background_highlights(cx),
995 &[
996 (
997 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
998 Color::red(),
999 ),
1000 (
1001 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1002 Color::red(),
1003 ),
1004 (
1005 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1006 Color::red(),
1007 ),
1008 ]
1009 );
1010 });
1011
1012 editor.update(cx, |editor, cx| {
1013 editor.change_selections(None, cx, |s| {
1014 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1015 });
1016 });
1017 search_bar.update(cx, |search_bar, cx| {
1018 assert_eq!(search_bar.active_match_index, Some(0));
1019 search_bar.select_next_match(&SelectNextMatch, cx);
1020 assert_eq!(
1021 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1022 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1023 );
1024 });
1025 search_bar.read_with(cx, |search_bar, _| {
1026 assert_eq!(search_bar.active_match_index, Some(0));
1027 });
1028
1029 search_bar.update(cx, |search_bar, cx| {
1030 search_bar.select_next_match(&SelectNextMatch, cx);
1031 assert_eq!(
1032 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1033 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1034 );
1035 });
1036 search_bar.read_with(cx, |search_bar, _| {
1037 assert_eq!(search_bar.active_match_index, Some(1));
1038 });
1039
1040 search_bar.update(cx, |search_bar, cx| {
1041 search_bar.select_next_match(&SelectNextMatch, cx);
1042 assert_eq!(
1043 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1044 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1045 );
1046 });
1047 search_bar.read_with(cx, |search_bar, _| {
1048 assert_eq!(search_bar.active_match_index, Some(2));
1049 });
1050
1051 search_bar.update(cx, |search_bar, cx| {
1052 search_bar.select_next_match(&SelectNextMatch, cx);
1053 assert_eq!(
1054 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1055 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1056 );
1057 });
1058 search_bar.read_with(cx, |search_bar, _| {
1059 assert_eq!(search_bar.active_match_index, Some(0));
1060 });
1061
1062 search_bar.update(cx, |search_bar, cx| {
1063 search_bar.select_prev_match(&SelectPrevMatch, cx);
1064 assert_eq!(
1065 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1066 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1067 );
1068 });
1069 search_bar.read_with(cx, |search_bar, _| {
1070 assert_eq!(search_bar.active_match_index, Some(2));
1071 });
1072
1073 search_bar.update(cx, |search_bar, cx| {
1074 search_bar.select_prev_match(&SelectPrevMatch, cx);
1075 assert_eq!(
1076 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1077 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1078 );
1079 });
1080 search_bar.read_with(cx, |search_bar, _| {
1081 assert_eq!(search_bar.active_match_index, Some(1));
1082 });
1083
1084 search_bar.update(cx, |search_bar, cx| {
1085 search_bar.select_prev_match(&SelectPrevMatch, cx);
1086 assert_eq!(
1087 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1088 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1089 );
1090 });
1091 search_bar.read_with(cx, |search_bar, _| {
1092 assert_eq!(search_bar.active_match_index, Some(0));
1093 });
1094
1095 // Park the cursor in between matches and ensure that going to the previous match selects
1096 // the closest match to the left.
1097 editor.update(cx, |editor, cx| {
1098 editor.change_selections(None, cx, |s| {
1099 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1100 });
1101 });
1102 search_bar.update(cx, |search_bar, cx| {
1103 assert_eq!(search_bar.active_match_index, Some(1));
1104 search_bar.select_prev_match(&SelectPrevMatch, cx);
1105 assert_eq!(
1106 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1107 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1108 );
1109 });
1110 search_bar.read_with(cx, |search_bar, _| {
1111 assert_eq!(search_bar.active_match_index, Some(0));
1112 });
1113
1114 // Park the cursor in between matches and ensure that going to the next match selects the
1115 // closest match to the right.
1116 editor.update(cx, |editor, cx| {
1117 editor.change_selections(None, cx, |s| {
1118 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1119 });
1120 });
1121 search_bar.update(cx, |search_bar, cx| {
1122 assert_eq!(search_bar.active_match_index, Some(1));
1123 search_bar.select_next_match(&SelectNextMatch, cx);
1124 assert_eq!(
1125 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1126 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1127 );
1128 });
1129 search_bar.read_with(cx, |search_bar, _| {
1130 assert_eq!(search_bar.active_match_index, Some(1));
1131 });
1132
1133 // Park the cursor after the last match and ensure that going to the previous match selects
1134 // the last match.
1135 editor.update(cx, |editor, cx| {
1136 editor.change_selections(None, cx, |s| {
1137 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1138 });
1139 });
1140 search_bar.update(cx, |search_bar, cx| {
1141 assert_eq!(search_bar.active_match_index, Some(2));
1142 search_bar.select_prev_match(&SelectPrevMatch, cx);
1143 assert_eq!(
1144 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1145 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1146 );
1147 });
1148 search_bar.read_with(cx, |search_bar, _| {
1149 assert_eq!(search_bar.active_match_index, Some(2));
1150 });
1151
1152 // Park the cursor after the last match and ensure that going to the next match selects the
1153 // first match.
1154 editor.update(cx, |editor, cx| {
1155 editor.change_selections(None, cx, |s| {
1156 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1157 });
1158 });
1159 search_bar.update(cx, |search_bar, cx| {
1160 assert_eq!(search_bar.active_match_index, Some(2));
1161 search_bar.select_next_match(&SelectNextMatch, cx);
1162 assert_eq!(
1163 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1164 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1165 );
1166 });
1167 search_bar.read_with(cx, |search_bar, _| {
1168 assert_eq!(search_bar.active_match_index, Some(0));
1169 });
1170
1171 // Park the cursor before the first match and ensure that going to the previous match
1172 // selects the last match.
1173 editor.update(cx, |editor, cx| {
1174 editor.change_selections(None, cx, |s| {
1175 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1176 });
1177 });
1178 search_bar.update(cx, |search_bar, cx| {
1179 assert_eq!(search_bar.active_match_index, Some(0));
1180 search_bar.select_prev_match(&SelectPrevMatch, cx);
1181 assert_eq!(
1182 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1183 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1184 );
1185 });
1186 search_bar.read_with(cx, |search_bar, _| {
1187 assert_eq!(search_bar.active_match_index, Some(2));
1188 });
1189 }
1190
1191 #[gpui::test]
1192 async fn test_search_option_handling(cx: &mut TestAppContext) {
1193 let (editor, search_bar) = init_test(cx);
1194
1195 // show with options should make current search case sensitive
1196 search_bar
1197 .update(cx, |search_bar, cx| {
1198 search_bar.show(cx);
1199 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1200 })
1201 .await
1202 .unwrap();
1203 editor.update(cx, |editor, cx| {
1204 assert_eq!(
1205 editor.all_background_highlights(cx),
1206 &[(
1207 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1208 Color::red(),
1209 )]
1210 );
1211 });
1212
1213 // search_suggested should restore default options
1214 search_bar.update(cx, |search_bar, cx| {
1215 search_bar.search_suggested(cx);
1216 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1217 });
1218
1219 // toggling a search option should update the defaults
1220 search_bar
1221 .update(cx, |search_bar, cx| {
1222 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1223 })
1224 .await
1225 .unwrap();
1226 search_bar.update(cx, |search_bar, cx| {
1227 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1228 });
1229 editor.next_notification(cx).await;
1230 editor.update(cx, |editor, cx| {
1231 assert_eq!(
1232 editor.all_background_highlights(cx),
1233 &[(
1234 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1235 Color::red(),
1236 ),]
1237 );
1238 });
1239
1240 // defaults should still include whole word
1241 search_bar.update(cx, |search_bar, cx| {
1242 search_bar.search_suggested(cx);
1243 assert_eq!(
1244 search_bar.search_options,
1245 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1246 )
1247 });
1248 }
1249
1250 #[gpui::test]
1251 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1252 crate::project_search::tests::init_test(cx);
1253
1254 let buffer_text = r#"
1255 A regular expression (shortened as regex or regexp;[1] also referred to as
1256 rational expression[2][3]) is a sequence of characters that specifies a search
1257 pattern in text. Usually such patterns are used by string-searching algorithms
1258 for "find" or "find and replace" operations on strings, or for input validation.
1259 "#
1260 .unindent();
1261 let expected_query_matches_count = buffer_text
1262 .chars()
1263 .filter(|c| c.to_ascii_lowercase() == 'a')
1264 .count();
1265 assert!(
1266 expected_query_matches_count > 1,
1267 "Should pick a query with multiple results"
1268 );
1269 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1270 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1271
1272 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1273
1274 let search_bar = cx.add_view(window_id, |cx| {
1275 let mut search_bar = BufferSearchBar::new(cx);
1276 search_bar.set_active_pane_item(Some(&editor), cx);
1277 search_bar.show(cx);
1278 search_bar
1279 });
1280
1281 search_bar
1282 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1283 .await
1284 .unwrap();
1285 search_bar.update(cx, |search_bar, cx| {
1286 cx.focus(search_bar.query_editor.as_any());
1287 search_bar.activate_current_match(cx);
1288 });
1289
1290 cx.read_window(window_id, |cx| {
1291 assert!(
1292 !editor.is_focused(cx),
1293 "Initially, the editor should not be focused"
1294 );
1295 });
1296 let initial_selections = editor.update(cx, |editor, cx| {
1297 let initial_selections = editor.selections.display_ranges(cx);
1298 assert_eq!(
1299 initial_selections.len(), 1,
1300 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1301 );
1302 initial_selections
1303 });
1304 search_bar.update(cx, |search_bar, _| {
1305 assert_eq!(search_bar.active_match_index, Some(0));
1306 });
1307
1308 search_bar.update(cx, |search_bar, cx| {
1309 cx.focus(search_bar.query_editor.as_any());
1310 search_bar.select_all_matches(&SelectAllMatches, cx);
1311 });
1312 cx.read_window(window_id, |cx| {
1313 assert!(
1314 editor.is_focused(cx),
1315 "Should focus editor after successful SelectAllMatches"
1316 );
1317 });
1318 search_bar.update(cx, |search_bar, cx| {
1319 let all_selections =
1320 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1321 assert_eq!(
1322 all_selections.len(),
1323 expected_query_matches_count,
1324 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1325 );
1326 assert_eq!(
1327 search_bar.active_match_index,
1328 Some(0),
1329 "Match index should not change after selecting all matches"
1330 );
1331 });
1332
1333 search_bar.update(cx, |search_bar, cx| {
1334 search_bar.select_next_match(&SelectNextMatch, cx);
1335 });
1336 cx.read_window(window_id, |cx| {
1337 assert!(
1338 editor.is_focused(cx),
1339 "Should still have editor focused after SelectNextMatch"
1340 );
1341 });
1342 search_bar.update(cx, |search_bar, cx| {
1343 let all_selections =
1344 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1345 assert_eq!(
1346 all_selections.len(),
1347 1,
1348 "On next match, should deselect items and select the next match"
1349 );
1350 assert_ne!(
1351 all_selections, initial_selections,
1352 "Next match should be different from the first selection"
1353 );
1354 assert_eq!(
1355 search_bar.active_match_index,
1356 Some(1),
1357 "Match index should be updated to the next one"
1358 );
1359 });
1360
1361 search_bar.update(cx, |search_bar, cx| {
1362 cx.focus(search_bar.query_editor.as_any());
1363 search_bar.select_all_matches(&SelectAllMatches, cx);
1364 });
1365 cx.read_window(window_id, |cx| {
1366 assert!(
1367 editor.is_focused(cx),
1368 "Should focus editor after successful SelectAllMatches"
1369 );
1370 });
1371 search_bar.update(cx, |search_bar, cx| {
1372 let all_selections =
1373 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1374 assert_eq!(
1375 all_selections.len(),
1376 expected_query_matches_count,
1377 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1378 );
1379 assert_eq!(
1380 search_bar.active_match_index,
1381 Some(1),
1382 "Match index should not change after selecting all matches"
1383 );
1384 });
1385
1386 search_bar.update(cx, |search_bar, cx| {
1387 search_bar.select_prev_match(&SelectPrevMatch, cx);
1388 });
1389 cx.read_window(window_id, |cx| {
1390 assert!(
1391 editor.is_focused(cx),
1392 "Should still have editor focused after SelectPrevMatch"
1393 );
1394 });
1395 let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1396 let all_selections =
1397 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1398 assert_eq!(
1399 all_selections.len(),
1400 1,
1401 "On previous match, should deselect items and select the previous item"
1402 );
1403 assert_eq!(
1404 all_selections, initial_selections,
1405 "Previous match should be the same as the first selection"
1406 );
1407 assert_eq!(
1408 search_bar.active_match_index,
1409 Some(0),
1410 "Match index should be updated to the previous one"
1411 );
1412 all_selections
1413 });
1414
1415 search_bar
1416 .update(cx, |search_bar, cx| {
1417 cx.focus(search_bar.query_editor.as_any());
1418 search_bar.search("abas_nonexistent_match", None, cx)
1419 })
1420 .await
1421 .unwrap();
1422 search_bar.update(cx, |search_bar, cx| {
1423 search_bar.select_all_matches(&SelectAllMatches, cx);
1424 });
1425 cx.read_window(window_id, |cx| {
1426 assert!(
1427 !editor.is_focused(cx),
1428 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1429 );
1430 });
1431 search_bar.update(cx, |search_bar, cx| {
1432 let all_selections =
1433 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1434 assert_eq!(
1435 all_selections, last_match_selections,
1436 "Should not select anything new if there are no matches"
1437 );
1438 assert!(
1439 search_bar.active_match_index.is_none(),
1440 "For no matches, there should be no active match index"
1441 );
1442 });
1443 }
1444
1445 #[gpui::test]
1446 async fn test_search_query_history(cx: &mut TestAppContext) {
1447 crate::project_search::tests::init_test(cx);
1448
1449 let buffer_text = r#"
1450 A regular expression (shortened as regex or regexp;[1] also referred to as
1451 rational expression[2][3]) is a sequence of characters that specifies a search
1452 pattern in text. Usually such patterns are used by string-searching algorithms
1453 for "find" or "find and replace" operations on strings, or for input validation.
1454 "#
1455 .unindent();
1456 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1457 let (window_id, _root_view) = cx.add_window(|_| EmptyView);
1458
1459 let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1460
1461 let search_bar = cx.add_view(window_id, |cx| {
1462 let mut search_bar = BufferSearchBar::new(cx);
1463 search_bar.set_active_pane_item(Some(&editor), cx);
1464 search_bar.show(cx);
1465 search_bar
1466 });
1467
1468 // Add 3 search items into the history.
1469 search_bar
1470 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1471 .await
1472 .unwrap();
1473 search_bar
1474 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1475 .await
1476 .unwrap();
1477 search_bar
1478 .update(cx, |search_bar, cx| {
1479 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1480 })
1481 .await
1482 .unwrap();
1483 // Ensure that the latest search is active.
1484 search_bar.read_with(cx, |search_bar, cx| {
1485 assert_eq!(search_bar.query(cx), "c");
1486 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1487 });
1488
1489 // Next history query after the latest should set the query to the empty string.
1490 search_bar.update(cx, |search_bar, cx| {
1491 search_bar.next_history_query(&NextHistoryQuery, cx);
1492 });
1493 search_bar.read_with(cx, |search_bar, cx| {
1494 assert_eq!(search_bar.query(cx), "");
1495 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1496 });
1497 search_bar.update(cx, |search_bar, cx| {
1498 search_bar.next_history_query(&NextHistoryQuery, cx);
1499 });
1500 search_bar.read_with(cx, |search_bar, cx| {
1501 assert_eq!(search_bar.query(cx), "");
1502 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1503 });
1504
1505 // First previous query for empty current query should set the query to the latest.
1506 search_bar.update(cx, |search_bar, cx| {
1507 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1508 });
1509 search_bar.read_with(cx, |search_bar, cx| {
1510 assert_eq!(search_bar.query(cx), "c");
1511 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1512 });
1513
1514 // Further previous items should go over the history in reverse order.
1515 search_bar.update(cx, |search_bar, cx| {
1516 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1517 });
1518 search_bar.read_with(cx, |search_bar, cx| {
1519 assert_eq!(search_bar.query(cx), "b");
1520 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1521 });
1522
1523 // Previous items should never go behind the first history item.
1524 search_bar.update(cx, |search_bar, cx| {
1525 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1526 });
1527 search_bar.read_with(cx, |search_bar, cx| {
1528 assert_eq!(search_bar.query(cx), "a");
1529 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1530 });
1531 search_bar.update(cx, |search_bar, cx| {
1532 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1533 });
1534 search_bar.read_with(cx, |search_bar, cx| {
1535 assert_eq!(search_bar.query(cx), "a");
1536 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1537 });
1538
1539 // Next items should go over the history in the original order.
1540 search_bar.update(cx, |search_bar, cx| {
1541 search_bar.next_history_query(&NextHistoryQuery, cx);
1542 });
1543 search_bar.read_with(cx, |search_bar, cx| {
1544 assert_eq!(search_bar.query(cx), "b");
1545 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1546 });
1547
1548 search_bar
1549 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1550 .await
1551 .unwrap();
1552 search_bar.read_with(cx, |search_bar, cx| {
1553 assert_eq!(search_bar.query(cx), "ba");
1554 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1555 });
1556
1557 // New search input should add another entry to history and move the selection to the end of the history.
1558 search_bar.update(cx, |search_bar, cx| {
1559 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1560 });
1561 search_bar.read_with(cx, |search_bar, cx| {
1562 assert_eq!(search_bar.query(cx), "c");
1563 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1564 });
1565 search_bar.update(cx, |search_bar, cx| {
1566 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1567 });
1568 search_bar.read_with(cx, |search_bar, cx| {
1569 assert_eq!(search_bar.query(cx), "b");
1570 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1571 });
1572 search_bar.update(cx, |search_bar, cx| {
1573 search_bar.next_history_query(&NextHistoryQuery, cx);
1574 });
1575 search_bar.read_with(cx, |search_bar, cx| {
1576 assert_eq!(search_bar.query(cx), "c");
1577 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1578 });
1579 search_bar.update(cx, |search_bar, cx| {
1580 search_bar.next_history_query(&NextHistoryQuery, cx);
1581 });
1582 search_bar.read_with(cx, |search_bar, cx| {
1583 assert_eq!(search_bar.query(cx), "ba");
1584 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1585 });
1586 search_bar.update(cx, |search_bar, cx| {
1587 search_bar.next_history_query(&NextHistoryQuery, cx);
1588 });
1589 search_bar.read_with(cx, |search_bar, cx| {
1590 assert_eq!(search_bar.query(cx), "");
1591 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1592 });
1593 }
1594}