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