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