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