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