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