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