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