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