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