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