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