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