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