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