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