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