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