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