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::new::<Self, _>(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::new::<NavButton, _>(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::new::<ActionButton, _>(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::new::<CloseButton, _>(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 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
854
855 let search_bar = window.add_view(cx, |cx| {
856 let mut search_bar = BufferSearchBar::new(cx);
857 search_bar.set_active_pane_item(Some(&editor), cx);
858 search_bar.show(cx);
859 search_bar
860 });
861
862 (editor, search_bar)
863 }
864
865 #[gpui::test]
866 async fn test_search_simple(cx: &mut TestAppContext) {
867 let (editor, search_bar) = init_test(cx);
868
869 // Search for a string that appears with different casing.
870 // By default, search is case-insensitive.
871 search_bar
872 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
873 .await
874 .unwrap();
875 editor.update(cx, |editor, cx| {
876 assert_eq!(
877 editor.all_background_highlights(cx),
878 &[
879 (
880 DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
881 Color::red(),
882 ),
883 (
884 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
885 Color::red(),
886 ),
887 ]
888 );
889 });
890
891 // Switch to a case sensitive search.
892 search_bar.update(cx, |search_bar, cx| {
893 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
894 });
895 editor.next_notification(cx).await;
896 editor.update(cx, |editor, cx| {
897 assert_eq!(
898 editor.all_background_highlights(cx),
899 &[(
900 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
901 Color::red(),
902 )]
903 );
904 });
905
906 // Search for a string that appears both as a whole word and
907 // within other words. By default, all results are found.
908 search_bar
909 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
910 .await
911 .unwrap();
912 editor.update(cx, |editor, cx| {
913 assert_eq!(
914 editor.all_background_highlights(cx),
915 &[
916 (
917 DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
918 Color::red(),
919 ),
920 (
921 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
922 Color::red(),
923 ),
924 (
925 DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
926 Color::red(),
927 ),
928 (
929 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
930 Color::red(),
931 ),
932 (
933 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
934 Color::red(),
935 ),
936 (
937 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
938 Color::red(),
939 ),
940 (
941 DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
942 Color::red(),
943 ),
944 ]
945 );
946 });
947
948 // Switch to a whole word search.
949 search_bar.update(cx, |search_bar, cx| {
950 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
951 });
952 editor.next_notification(cx).await;
953 editor.update(cx, |editor, cx| {
954 assert_eq!(
955 editor.all_background_highlights(cx),
956 &[
957 (
958 DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
959 Color::red(),
960 ),
961 (
962 DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
963 Color::red(),
964 ),
965 (
966 DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
967 Color::red(),
968 ),
969 ]
970 );
971 });
972
973 editor.update(cx, |editor, cx| {
974 editor.change_selections(None, cx, |s| {
975 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
976 });
977 });
978 search_bar.update(cx, |search_bar, cx| {
979 assert_eq!(search_bar.active_match_index, Some(0));
980 search_bar.select_next_match(&SelectNextMatch, cx);
981 assert_eq!(
982 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
983 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
984 );
985 });
986 search_bar.read_with(cx, |search_bar, _| {
987 assert_eq!(search_bar.active_match_index, Some(0));
988 });
989
990 search_bar.update(cx, |search_bar, cx| {
991 search_bar.select_next_match(&SelectNextMatch, cx);
992 assert_eq!(
993 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
994 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
995 );
996 });
997 search_bar.read_with(cx, |search_bar, _| {
998 assert_eq!(search_bar.active_match_index, Some(1));
999 });
1000
1001 search_bar.update(cx, |search_bar, cx| {
1002 search_bar.select_next_match(&SelectNextMatch, cx);
1003 assert_eq!(
1004 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1005 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1006 );
1007 });
1008 search_bar.read_with(cx, |search_bar, _| {
1009 assert_eq!(search_bar.active_match_index, Some(2));
1010 });
1011
1012 search_bar.update(cx, |search_bar, cx| {
1013 search_bar.select_next_match(&SelectNextMatch, cx);
1014 assert_eq!(
1015 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1016 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1017 );
1018 });
1019 search_bar.read_with(cx, |search_bar, _| {
1020 assert_eq!(search_bar.active_match_index, Some(0));
1021 });
1022
1023 search_bar.update(cx, |search_bar, cx| {
1024 search_bar.select_prev_match(&SelectPrevMatch, cx);
1025 assert_eq!(
1026 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1027 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1028 );
1029 });
1030 search_bar.read_with(cx, |search_bar, _| {
1031 assert_eq!(search_bar.active_match_index, Some(2));
1032 });
1033
1034 search_bar.update(cx, |search_bar, cx| {
1035 search_bar.select_prev_match(&SelectPrevMatch, cx);
1036 assert_eq!(
1037 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1038 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1039 );
1040 });
1041 search_bar.read_with(cx, |search_bar, _| {
1042 assert_eq!(search_bar.active_match_index, Some(1));
1043 });
1044
1045 search_bar.update(cx, |search_bar, cx| {
1046 search_bar.select_prev_match(&SelectPrevMatch, cx);
1047 assert_eq!(
1048 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1049 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1050 );
1051 });
1052 search_bar.read_with(cx, |search_bar, _| {
1053 assert_eq!(search_bar.active_match_index, Some(0));
1054 });
1055
1056 // Park the cursor in between matches and ensure that going to the previous match selects
1057 // the closest match to the left.
1058 editor.update(cx, |editor, cx| {
1059 editor.change_selections(None, cx, |s| {
1060 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1061 });
1062 });
1063 search_bar.update(cx, |search_bar, cx| {
1064 assert_eq!(search_bar.active_match_index, Some(1));
1065 search_bar.select_prev_match(&SelectPrevMatch, cx);
1066 assert_eq!(
1067 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1068 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1069 );
1070 });
1071 search_bar.read_with(cx, |search_bar, _| {
1072 assert_eq!(search_bar.active_match_index, Some(0));
1073 });
1074
1075 // Park the cursor in between matches and ensure that going to the next match selects the
1076 // closest match to the right.
1077 editor.update(cx, |editor, cx| {
1078 editor.change_selections(None, cx, |s| {
1079 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1080 });
1081 });
1082 search_bar.update(cx, |search_bar, cx| {
1083 assert_eq!(search_bar.active_match_index, Some(1));
1084 search_bar.select_next_match(&SelectNextMatch, cx);
1085 assert_eq!(
1086 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1087 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1088 );
1089 });
1090 search_bar.read_with(cx, |search_bar, _| {
1091 assert_eq!(search_bar.active_match_index, Some(1));
1092 });
1093
1094 // Park the cursor after the last match and ensure that going to the previous match selects
1095 // the last match.
1096 editor.update(cx, |editor, cx| {
1097 editor.change_selections(None, cx, |s| {
1098 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1099 });
1100 });
1101 search_bar.update(cx, |search_bar, cx| {
1102 assert_eq!(search_bar.active_match_index, Some(2));
1103 search_bar.select_prev_match(&SelectPrevMatch, cx);
1104 assert_eq!(
1105 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1106 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1107 );
1108 });
1109 search_bar.read_with(cx, |search_bar, _| {
1110 assert_eq!(search_bar.active_match_index, Some(2));
1111 });
1112
1113 // Park the cursor after the last match and ensure that going to the next match selects the
1114 // first match.
1115 editor.update(cx, |editor, cx| {
1116 editor.change_selections(None, cx, |s| {
1117 s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1118 });
1119 });
1120 search_bar.update(cx, |search_bar, cx| {
1121 assert_eq!(search_bar.active_match_index, Some(2));
1122 search_bar.select_next_match(&SelectNextMatch, cx);
1123 assert_eq!(
1124 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1125 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1126 );
1127 });
1128 search_bar.read_with(cx, |search_bar, _| {
1129 assert_eq!(search_bar.active_match_index, Some(0));
1130 });
1131
1132 // Park the cursor before the first match and ensure that going to the previous match
1133 // selects the last match.
1134 editor.update(cx, |editor, cx| {
1135 editor.change_selections(None, cx, |s| {
1136 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1137 });
1138 });
1139 search_bar.update(cx, |search_bar, cx| {
1140 assert_eq!(search_bar.active_match_index, Some(0));
1141 search_bar.select_prev_match(&SelectPrevMatch, cx);
1142 assert_eq!(
1143 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1144 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1145 );
1146 });
1147 search_bar.read_with(cx, |search_bar, _| {
1148 assert_eq!(search_bar.active_match_index, Some(2));
1149 });
1150 }
1151
1152 #[gpui::test]
1153 async fn test_search_option_handling(cx: &mut TestAppContext) {
1154 let (editor, search_bar) = init_test(cx);
1155
1156 // show with options should make current search case sensitive
1157 search_bar
1158 .update(cx, |search_bar, cx| {
1159 search_bar.show(cx);
1160 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1161 })
1162 .await
1163 .unwrap();
1164 editor.update(cx, |editor, cx| {
1165 assert_eq!(
1166 editor.all_background_highlights(cx),
1167 &[(
1168 DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1169 Color::red(),
1170 )]
1171 );
1172 });
1173
1174 // search_suggested should restore default options
1175 search_bar.update(cx, |search_bar, cx| {
1176 search_bar.search_suggested(cx);
1177 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1178 });
1179
1180 // toggling a search option should update the defaults
1181 search_bar
1182 .update(cx, |search_bar, cx| {
1183 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1184 })
1185 .await
1186 .unwrap();
1187 search_bar.update(cx, |search_bar, cx| {
1188 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1189 });
1190 editor.next_notification(cx).await;
1191 editor.update(cx, |editor, cx| {
1192 assert_eq!(
1193 editor.all_background_highlights(cx),
1194 &[(
1195 DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
1196 Color::red(),
1197 ),]
1198 );
1199 });
1200
1201 // defaults should still include whole word
1202 search_bar.update(cx, |search_bar, cx| {
1203 search_bar.search_suggested(cx);
1204 assert_eq!(
1205 search_bar.search_options,
1206 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1207 )
1208 });
1209 }
1210
1211 #[gpui::test]
1212 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1213 crate::project_search::tests::init_test(cx);
1214
1215 let buffer_text = r#"
1216 A regular expression (shortened as regex or regexp;[1] also referred to as
1217 rational expression[2][3]) is a sequence of characters that specifies a search
1218 pattern in text. Usually such patterns are used by string-searching algorithms
1219 for "find" or "find and replace" operations on strings, or for input validation.
1220 "#
1221 .unindent();
1222 let expected_query_matches_count = buffer_text
1223 .chars()
1224 .filter(|c| c.to_ascii_lowercase() == 'a')
1225 .count();
1226 assert!(
1227 expected_query_matches_count > 1,
1228 "Should pick a query with multiple results"
1229 );
1230 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1231 let window = cx.add_window(|_| EmptyView);
1232 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1233
1234 let search_bar = window.add_view(cx, |cx| {
1235 let mut search_bar = BufferSearchBar::new(cx);
1236 search_bar.set_active_pane_item(Some(&editor), cx);
1237 search_bar.show(cx);
1238 search_bar
1239 });
1240
1241 search_bar
1242 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1243 .await
1244 .unwrap();
1245 search_bar.update(cx, |search_bar, cx| {
1246 cx.focus(search_bar.query_editor.as_any());
1247 search_bar.activate_current_match(cx);
1248 });
1249
1250 window.read_with(cx, |cx| {
1251 assert!(
1252 !editor.is_focused(cx),
1253 "Initially, the editor should not be focused"
1254 );
1255 });
1256
1257 let initial_selections = editor.update(cx, |editor, cx| {
1258 let initial_selections = editor.selections.display_ranges(cx);
1259 assert_eq!(
1260 initial_selections.len(), 1,
1261 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1262 );
1263 initial_selections
1264 });
1265 search_bar.update(cx, |search_bar, _| {
1266 assert_eq!(search_bar.active_match_index, Some(0));
1267 });
1268
1269 search_bar.update(cx, |search_bar, cx| {
1270 cx.focus(search_bar.query_editor.as_any());
1271 search_bar.select_all_matches(&SelectAllMatches, cx);
1272 });
1273 window.read_with(cx, |cx| {
1274 assert!(
1275 editor.is_focused(cx),
1276 "Should focus editor after successful SelectAllMatches"
1277 );
1278 });
1279 search_bar.update(cx, |search_bar, cx| {
1280 let all_selections =
1281 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1282 assert_eq!(
1283 all_selections.len(),
1284 expected_query_matches_count,
1285 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1286 );
1287 assert_eq!(
1288 search_bar.active_match_index,
1289 Some(0),
1290 "Match index should not change after selecting all matches"
1291 );
1292 });
1293
1294 search_bar.update(cx, |search_bar, cx| {
1295 search_bar.select_next_match(&SelectNextMatch, cx);
1296 });
1297 window.read_with(cx, |cx| {
1298 assert!(
1299 editor.is_focused(cx),
1300 "Should still have editor focused after SelectNextMatch"
1301 );
1302 });
1303 search_bar.update(cx, |search_bar, cx| {
1304 let all_selections =
1305 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1306 assert_eq!(
1307 all_selections.len(),
1308 1,
1309 "On next match, should deselect items and select the next match"
1310 );
1311 assert_ne!(
1312 all_selections, initial_selections,
1313 "Next match should be different from the first selection"
1314 );
1315 assert_eq!(
1316 search_bar.active_match_index,
1317 Some(1),
1318 "Match index should be updated to the next one"
1319 );
1320 });
1321
1322 search_bar.update(cx, |search_bar, cx| {
1323 cx.focus(search_bar.query_editor.as_any());
1324 search_bar.select_all_matches(&SelectAllMatches, cx);
1325 });
1326 window.read_with(cx, |cx| {
1327 assert!(
1328 editor.is_focused(cx),
1329 "Should focus editor after successful SelectAllMatches"
1330 );
1331 });
1332 search_bar.update(cx, |search_bar, cx| {
1333 let all_selections =
1334 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1335 assert_eq!(
1336 all_selections.len(),
1337 expected_query_matches_count,
1338 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1339 );
1340 assert_eq!(
1341 search_bar.active_match_index,
1342 Some(1),
1343 "Match index should not change after selecting all matches"
1344 );
1345 });
1346
1347 search_bar.update(cx, |search_bar, cx| {
1348 search_bar.select_prev_match(&SelectPrevMatch, cx);
1349 });
1350 window.read_with(cx, |cx| {
1351 assert!(
1352 editor.is_focused(cx),
1353 "Should still have editor focused after SelectPrevMatch"
1354 );
1355 });
1356 let last_match_selections = search_bar.update(cx, |search_bar, cx| {
1357 let all_selections =
1358 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1359 assert_eq!(
1360 all_selections.len(),
1361 1,
1362 "On previous match, should deselect items and select the previous item"
1363 );
1364 assert_eq!(
1365 all_selections, initial_selections,
1366 "Previous match should be the same as the first selection"
1367 );
1368 assert_eq!(
1369 search_bar.active_match_index,
1370 Some(0),
1371 "Match index should be updated to the previous one"
1372 );
1373 all_selections
1374 });
1375
1376 search_bar
1377 .update(cx, |search_bar, cx| {
1378 cx.focus(search_bar.query_editor.as_any());
1379 search_bar.search("abas_nonexistent_match", None, cx)
1380 })
1381 .await
1382 .unwrap();
1383 search_bar.update(cx, |search_bar, cx| {
1384 search_bar.select_all_matches(&SelectAllMatches, cx);
1385 });
1386 window.read_with(cx, |cx| {
1387 assert!(
1388 !editor.is_focused(cx),
1389 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1390 );
1391 });
1392 search_bar.update(cx, |search_bar, cx| {
1393 let all_selections =
1394 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1395 assert_eq!(
1396 all_selections, last_match_selections,
1397 "Should not select anything new if there are no matches"
1398 );
1399 assert!(
1400 search_bar.active_match_index.is_none(),
1401 "For no matches, there should be no active match index"
1402 );
1403 });
1404 }
1405
1406 #[gpui::test]
1407 async fn test_search_query_history(cx: &mut TestAppContext) {
1408 crate::project_search::tests::init_test(cx);
1409
1410 let buffer_text = r#"
1411 A regular expression (shortened as regex or regexp;[1] also referred to as
1412 rational expression[2][3]) is a sequence of characters that specifies a search
1413 pattern in text. Usually such patterns are used by string-searching algorithms
1414 for "find" or "find and replace" operations on strings, or for input validation.
1415 "#
1416 .unindent();
1417 let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
1418 let window = cx.add_window(|_| EmptyView);
1419
1420 let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1421
1422 let search_bar = window.add_view(cx, |cx| {
1423 let mut search_bar = BufferSearchBar::new(cx);
1424 search_bar.set_active_pane_item(Some(&editor), cx);
1425 search_bar.show(cx);
1426 search_bar
1427 });
1428
1429 // Add 3 search items into the history.
1430 search_bar
1431 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1432 .await
1433 .unwrap();
1434 search_bar
1435 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1436 .await
1437 .unwrap();
1438 search_bar
1439 .update(cx, |search_bar, cx| {
1440 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1441 })
1442 .await
1443 .unwrap();
1444 // Ensure that the latest search is active.
1445 search_bar.read_with(cx, |search_bar, cx| {
1446 assert_eq!(search_bar.query(cx), "c");
1447 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1448 });
1449
1450 // Next history query after the latest should set the query to the empty string.
1451 search_bar.update(cx, |search_bar, cx| {
1452 search_bar.next_history_query(&NextHistoryQuery, cx);
1453 });
1454 search_bar.read_with(cx, |search_bar, cx| {
1455 assert_eq!(search_bar.query(cx), "");
1456 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1457 });
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
1466 // First previous query for empty current query should set the query to the latest.
1467 search_bar.update(cx, |search_bar, cx| {
1468 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1469 });
1470 search_bar.read_with(cx, |search_bar, cx| {
1471 assert_eq!(search_bar.query(cx), "c");
1472 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1473 });
1474
1475 // Further previous items should go over the history in reverse order.
1476 search_bar.update(cx, |search_bar, cx| {
1477 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1478 });
1479 search_bar.read_with(cx, |search_bar, cx| {
1480 assert_eq!(search_bar.query(cx), "b");
1481 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1482 });
1483
1484 // Previous items should never go behind the first history item.
1485 search_bar.update(cx, |search_bar, cx| {
1486 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1487 });
1488 search_bar.read_with(cx, |search_bar, cx| {
1489 assert_eq!(search_bar.query(cx), "a");
1490 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1491 });
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
1500 // Next items should go over the history in the original order.
1501 search_bar.update(cx, |search_bar, cx| {
1502 search_bar.next_history_query(&NextHistoryQuery, cx);
1503 });
1504 search_bar.read_with(cx, |search_bar, cx| {
1505 assert_eq!(search_bar.query(cx), "b");
1506 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1507 });
1508
1509 search_bar
1510 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1511 .await
1512 .unwrap();
1513 search_bar.read_with(cx, |search_bar, cx| {
1514 assert_eq!(search_bar.query(cx), "ba");
1515 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1516 });
1517
1518 // New search input should add another entry to history and move the selection to the end of the history.
1519 search_bar.update(cx, |search_bar, cx| {
1520 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1521 });
1522 search_bar.read_with(cx, |search_bar, cx| {
1523 assert_eq!(search_bar.query(cx), "c");
1524 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1525 });
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), "b");
1531 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1532 });
1533 search_bar.update(cx, |search_bar, cx| {
1534 search_bar.next_history_query(&NextHistoryQuery, cx);
1535 });
1536 search_bar.read_with(cx, |search_bar, cx| {
1537 assert_eq!(search_bar.query(cx), "c");
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), "ba");
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), "");
1552 assert_eq!(search_bar.search_options, SearchOptions::NONE);
1553 });
1554 }
1555}