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