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