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