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