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