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