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