1mod registrar;
2
3use crate::{
4 search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
5 ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
6 ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7};
8use any_vec::AnyVec;
9use collections::HashMap;
10use editor::{
11 actions::{Tab, TabPrev},
12 DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
13};
14use futures::channel::oneshot;
15use gpui::{
16 actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusHandle,
17 FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
18 Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext,
19 VisualContext as _, WindowContext,
20};
21use project::{
22 search::SearchQuery,
23 search_history::{SearchHistory, SearchHistoryCursor},
24};
25use serde::Deserialize;
26use settings::Settings;
27use std::sync::Arc;
28use theme::ThemeSettings;
29
30use ui::{
31 h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip,
32 BASE_REM_SIZE_IN_PX,
33};
34use util::ResultExt;
35use workspace::{
36 item::ItemHandle,
37 searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
38 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
39};
40
41pub use registrar::DivRegistrar;
42use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
43
44const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
45
46#[derive(PartialEq, Clone, Deserialize)]
47pub struct Deploy {
48 #[serde(default = "util::serde::default_true")]
49 pub focus: bool,
50 #[serde(default)]
51 pub replace_enabled: bool,
52 #[serde(default)]
53 pub selection_search_enabled: bool,
54}
55
56impl_actions!(buffer_search, [Deploy]);
57
58actions!(buffer_search, [Dismiss, FocusEditor]);
59
60impl Deploy {
61 pub fn find() -> Self {
62 Self {
63 focus: true,
64 replace_enabled: false,
65 selection_search_enabled: false,
66 }
67 }
68}
69
70pub enum Event {
71 UpdateLocation,
72}
73
74pub fn init(cx: &mut AppContext) {
75 cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
76 .detach();
77}
78
79pub struct BufferSearchBar {
80 query_editor: View<Editor>,
81 query_editor_focused: bool,
82 replacement_editor: View<Editor>,
83 replacement_editor_focused: bool,
84 active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
85 active_match_index: Option<usize>,
86 active_searchable_item_subscription: Option<Subscription>,
87 active_search: Option<Arc<SearchQuery>>,
88 searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
89 pending_search: Option<Task<()>>,
90 search_options: SearchOptions,
91 default_options: SearchOptions,
92 configured_options: SearchOptions,
93 query_contains_error: bool,
94 dismissed: bool,
95 search_history: SearchHistory,
96 search_history_cursor: SearchHistoryCursor,
97 replace_enabled: bool,
98 selection_search_enabled: bool,
99 scroll_handle: ScrollHandle,
100 editor_scroll_handle: ScrollHandle,
101 editor_needed_width: Pixels,
102}
103
104impl BufferSearchBar {
105 fn render_text_input(
106 &self,
107 editor: &View<Editor>,
108 color: Hsla,
109 cx: &ViewContext<Self>,
110 ) -> impl IntoElement {
111 let settings = ThemeSettings::get_global(cx);
112 let text_style = TextStyle {
113 color: if editor.read(cx).read_only(cx) {
114 cx.theme().colors().text_disabled
115 } else {
116 color
117 },
118 font_family: settings.buffer_font.family.clone(),
119 font_features: settings.buffer_font.features.clone(),
120 font_fallbacks: settings.buffer_font.fallbacks.clone(),
121 font_size: rems(0.875).into(),
122 font_weight: settings.buffer_font.weight,
123 line_height: relative(1.3),
124 ..Default::default()
125 };
126
127 EditorElement::new(
128 editor,
129 EditorStyle {
130 background: cx.theme().colors().editor_background,
131 local_player: cx.theme().players().local(),
132 text: text_style,
133 ..Default::default()
134 },
135 )
136 }
137
138 pub fn query_editor_focused(&self) -> bool {
139 self.query_editor_focused
140 }
141}
142
143impl EventEmitter<Event> for BufferSearchBar {}
144impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
145impl Render for BufferSearchBar {
146 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
147 if self.dismissed {
148 return div().id("search_bar");
149 }
150
151 let focus_handle = self.focus_handle(cx);
152
153 let narrow_mode =
154 self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
155 let hide_inline_icons = self.editor_needed_width
156 > self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
157
158 let supported_options = self.supported_options();
159
160 if self.query_editor.update(cx, |query_editor, cx| {
161 query_editor.placeholder_text(cx).is_none()
162 }) {
163 self.query_editor.update(cx, |editor, cx| {
164 editor.set_placeholder_text("Search…", cx);
165 });
166 }
167
168 self.replacement_editor.update(cx, |editor, cx| {
169 editor.set_placeholder_text("Replace with…", cx);
170 });
171
172 let mut text_color = Color::Default;
173 let match_text = self
174 .active_searchable_item
175 .as_ref()
176 .and_then(|searchable_item| {
177 if self.query(cx).is_empty() {
178 return None;
179 }
180 let matches_count = self
181 .searchable_items_with_matches
182 .get(&searchable_item.downgrade())
183 .map(AnyVec::len)
184 .unwrap_or(0);
185 if let Some(match_ix) = self.active_match_index {
186 Some(format!("{}/{}", match_ix + 1, matches_count))
187 } else {
188 text_color = Color::Error; // No matches found
189 None
190 }
191 })
192 .unwrap_or_else(|| "0/0".to_string());
193 let should_show_replace_input = self.replace_enabled && supported_options.replacement;
194 let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
195
196 let mut key_context = KeyContext::new_with_defaults();
197 key_context.add("BufferSearchBar");
198 if in_replace {
199 key_context.add("in_replace");
200 }
201 let editor_border = if self.query_contains_error {
202 Color::Error.color(cx)
203 } else {
204 cx.theme().colors().border
205 };
206
207 let container_width = cx.viewport_size().width;
208 let input_width = SearchInputWidth::calc_width(container_width);
209
210 let input_base_styles = || {
211 h_flex()
212 .min_w_32()
213 .w(input_width)
214 .h_8()
215 .px_2()
216 .py_1()
217 .border_1()
218 .border_color(editor_border)
219 .rounded_lg()
220 };
221
222 let search_line = h_flex()
223 .gap_2()
224 .child(
225 input_base_styles()
226 .id("editor-scroll")
227 .track_scroll(&self.editor_scroll_handle)
228 .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
229 .when(!hide_inline_icons, |div| {
230 div.children(supported_options.case.then(|| {
231 self.render_search_option_button(
232 SearchOptions::CASE_SENSITIVE,
233 focus_handle.clone(),
234 cx.listener(|this, _, cx| {
235 this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
236 }),
237 )
238 }))
239 .children(supported_options.word.then(|| {
240 self.render_search_option_button(
241 SearchOptions::WHOLE_WORD,
242 focus_handle.clone(),
243 cx.listener(|this, _, cx| {
244 this.toggle_whole_word(&ToggleWholeWord, cx)
245 }),
246 )
247 }))
248 .children(supported_options.regex.then(|| {
249 self.render_search_option_button(
250 SearchOptions::REGEX,
251 focus_handle.clone(),
252 cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
253 )
254 }))
255 }),
256 )
257 .child(
258 h_flex()
259 .gap_1()
260 .min_w_64()
261 .when(supported_options.replacement, |this| {
262 this.child(
263 IconButton::new(
264 "buffer-search-bar-toggle-replace-button",
265 IconName::Replace,
266 )
267 .style(ButtonStyle::Subtle)
268 .shape(IconButtonShape::Square)
269 .when(self.replace_enabled, |button| {
270 button.style(ButtonStyle::Filled)
271 })
272 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
273 this.toggle_replace(&ToggleReplace, cx);
274 }))
275 .toggle_state(self.replace_enabled)
276 .tooltip({
277 let focus_handle = focus_handle.clone();
278 move |cx| {
279 Tooltip::for_action_in(
280 "Toggle Replace",
281 &ToggleReplace,
282 &focus_handle,
283 cx,
284 )
285 }
286 }),
287 )
288 })
289 .when(supported_options.selection, |this| {
290 this.child(
291 IconButton::new(
292 "buffer-search-bar-toggle-search-selection-button",
293 IconName::SearchSelection,
294 )
295 .style(ButtonStyle::Subtle)
296 .shape(IconButtonShape::Square)
297 .when(self.selection_search_enabled, |button| {
298 button.style(ButtonStyle::Filled)
299 })
300 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
301 this.toggle_selection(&ToggleSelection, cx);
302 }))
303 .toggle_state(self.selection_search_enabled)
304 .tooltip({
305 let focus_handle = focus_handle.clone();
306 move |cx| {
307 Tooltip::for_action_in(
308 "Toggle Search Selection",
309 &ToggleSelection,
310 &focus_handle,
311 cx,
312 )
313 }
314 }),
315 )
316 })
317 .child(
318 IconButton::new("select-all", ui::IconName::SelectAll)
319 .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
320 .shape(IconButtonShape::Square)
321 .tooltip({
322 let focus_handle = focus_handle.clone();
323 move |cx| {
324 Tooltip::for_action_in(
325 "Select All Matches",
326 &SelectAllMatches,
327 &focus_handle,
328 cx,
329 )
330 }
331 }),
332 )
333 .child(
334 h_flex()
335 .pl_2()
336 .ml_2()
337 .border_l_1()
338 .border_color(cx.theme().colors().border_variant)
339 .child(render_nav_button(
340 ui::IconName::ChevronLeft,
341 self.active_match_index.is_some(),
342 "Select Previous Match",
343 &SelectPrevMatch,
344 focus_handle.clone(),
345 ))
346 .child(render_nav_button(
347 ui::IconName::ChevronRight,
348 self.active_match_index.is_some(),
349 "Select Next Match",
350 &SelectNextMatch,
351 focus_handle.clone(),
352 )),
353 )
354 .when(!narrow_mode, |this| {
355 this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
356 Label::new(match_text).size(LabelSize::Small).color(
357 if self.active_match_index.is_some() {
358 Color::Default
359 } else {
360 Color::Disabled
361 },
362 ),
363 ))
364 }),
365 );
366
367 let replace_line = should_show_replace_input.then(|| {
368 h_flex()
369 .gap_2()
370 .child(input_base_styles().child(self.render_text_input(
371 &self.replacement_editor,
372 cx.theme().colors().text,
373 cx,
374 )))
375 .child(
376 h_flex()
377 .min_w_64()
378 .gap_1()
379 .child(
380 IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
381 .shape(IconButtonShape::Square)
382 .tooltip({
383 let focus_handle = focus_handle.clone();
384 move |cx| {
385 Tooltip::for_action_in(
386 "Replace Next Match",
387 &ReplaceNext,
388 &focus_handle,
389 cx,
390 )
391 }
392 })
393 .on_click(
394 cx.listener(|this, _, cx| this.replace_next(&ReplaceNext, cx)),
395 ),
396 )
397 .child(
398 IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
399 .shape(IconButtonShape::Square)
400 .tooltip({
401 let focus_handle = focus_handle.clone();
402 move |cx| {
403 Tooltip::for_action_in(
404 "Replace All Matches",
405 &ReplaceAll,
406 &focus_handle,
407 cx,
408 )
409 }
410 })
411 .on_click(
412 cx.listener(|this, _, cx| this.replace_all(&ReplaceAll, cx)),
413 ),
414 ),
415 )
416 });
417
418 v_flex()
419 .id("buffer_search")
420 .gap_2()
421 .track_scroll(&self.scroll_handle)
422 .key_context(key_context)
423 .capture_action(cx.listener(Self::tab))
424 .capture_action(cx.listener(Self::tab_prev))
425 .on_action(cx.listener(Self::previous_history_query))
426 .on_action(cx.listener(Self::next_history_query))
427 .on_action(cx.listener(Self::dismiss))
428 .on_action(cx.listener(Self::select_next_match))
429 .on_action(cx.listener(Self::select_prev_match))
430 .when(self.supported_options().replacement, |this| {
431 this.on_action(cx.listener(Self::toggle_replace))
432 .when(in_replace, |this| {
433 this.on_action(cx.listener(Self::replace_next))
434 .on_action(cx.listener(Self::replace_all))
435 })
436 })
437 .when(self.supported_options().case, |this| {
438 this.on_action(cx.listener(Self::toggle_case_sensitive))
439 })
440 .when(self.supported_options().word, |this| {
441 this.on_action(cx.listener(Self::toggle_whole_word))
442 })
443 .when(self.supported_options().regex, |this| {
444 this.on_action(cx.listener(Self::toggle_regex))
445 })
446 .when(self.supported_options().selection, |this| {
447 this.on_action(cx.listener(Self::toggle_selection))
448 })
449 .child(
450 h_flex()
451 .relative()
452 .child(search_line.w_full())
453 .when(!narrow_mode, |div| {
454 div.child(
455 h_flex().absolute().right_0().child(
456 IconButton::new(SharedString::from("Close"), IconName::Close)
457 .shape(IconButtonShape::Square)
458 .tooltip(move |cx| {
459 Tooltip::for_action("Close Search Bar", &Dismiss, cx)
460 })
461 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
462 this.dismiss(&Dismiss, cx)
463 })),
464 ),
465 )
466 }),
467 )
468 .children(replace_line)
469 }
470}
471
472impl FocusableView for BufferSearchBar {
473 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
474 self.query_editor.focus_handle(cx)
475 }
476}
477
478impl ToolbarItemView for BufferSearchBar {
479 fn set_active_pane_item(
480 &mut self,
481 item: Option<&dyn ItemHandle>,
482 cx: &mut ViewContext<Self>,
483 ) -> ToolbarItemLocation {
484 cx.notify();
485 self.active_searchable_item_subscription.take();
486 self.active_searchable_item.take();
487
488 self.pending_search.take();
489
490 if let Some(searchable_item_handle) =
491 item.and_then(|item| item.to_searchable_item_handle(cx))
492 {
493 let this = cx.view().downgrade();
494
495 self.active_searchable_item_subscription =
496 Some(searchable_item_handle.subscribe_to_search_events(
497 cx,
498 Box::new(move |search_event, cx| {
499 if let Some(this) = this.upgrade() {
500 this.update(cx, |this, cx| {
501 this.on_active_searchable_item_event(search_event, cx)
502 });
503 }
504 }),
505 ));
506
507 self.active_searchable_item = Some(searchable_item_handle);
508 drop(self.update_matches(true, cx));
509 if !self.dismissed {
510 return ToolbarItemLocation::Secondary;
511 }
512 }
513 ToolbarItemLocation::Hidden
514 }
515}
516
517impl BufferSearchBar {
518 pub fn register(registrar: &mut impl SearchActionsRegistrar) {
519 registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
520 this.query_editor.focus_handle(cx).focus(cx);
521 this.select_query(cx);
522 }));
523 registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
524 if this.supported_options().case {
525 this.toggle_case_sensitive(action, cx);
526 }
527 }));
528 registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
529 if this.supported_options().word {
530 this.toggle_whole_word(action, cx);
531 }
532 }));
533 registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, cx| {
534 if this.supported_options().regex {
535 this.toggle_regex(action, cx);
536 }
537 }));
538 registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
539 if this.supported_options().selection {
540 this.toggle_selection(action, cx);
541 }
542 }));
543 registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
544 if this.supported_options().replacement {
545 this.toggle_replace(action, cx);
546 }
547 }));
548 registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
549 this.select_next_match(action, cx);
550 }));
551 registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
552 this.select_prev_match(action, cx);
553 }));
554 registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
555 this.select_all_matches(action, cx);
556 }));
557 registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
558 this.dismiss(&Dismiss, cx);
559 }));
560 registrar.register_handler(ForDeployed(|this, _: &Dismiss, cx| {
561 this.dismiss(&Dismiss, cx);
562 }));
563
564 // register deploy buffer search for both search bar states, since we want to focus into the search bar
565 // when the deploy action is triggered in the buffer.
566 registrar.register_handler(ForDeployed(|this, deploy, cx| {
567 this.deploy(deploy, cx);
568 }));
569 registrar.register_handler(ForDismissed(|this, deploy, cx| {
570 this.deploy(deploy, cx);
571 }))
572 }
573
574 pub fn new(cx: &mut ViewContext<Self>) -> Self {
575 let query_editor = cx.new_view(Editor::single_line);
576 cx.subscribe(&query_editor, Self::on_query_editor_event)
577 .detach();
578 let replacement_editor = cx.new_view(Editor::single_line);
579 cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
580 .detach();
581
582 let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
583
584 Self {
585 query_editor,
586 query_editor_focused: false,
587 replacement_editor,
588 replacement_editor_focused: false,
589 active_searchable_item: None,
590 active_searchable_item_subscription: None,
591 active_match_index: None,
592 searchable_items_with_matches: Default::default(),
593 default_options: search_options,
594 configured_options: search_options,
595 search_options,
596 pending_search: None,
597 query_contains_error: false,
598 dismissed: true,
599 search_history: SearchHistory::new(
600 Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
601 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
602 ),
603 search_history_cursor: Default::default(),
604 active_search: None,
605 replace_enabled: false,
606 selection_search_enabled: false,
607 scroll_handle: ScrollHandle::new(),
608 editor_scroll_handle: ScrollHandle::new(),
609 editor_needed_width: px(0.),
610 }
611 }
612
613 pub fn is_dismissed(&self) -> bool {
614 self.dismissed
615 }
616
617 pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
618 self.dismissed = true;
619 for searchable_item in self.searchable_items_with_matches.keys() {
620 if let Some(searchable_item) =
621 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
622 {
623 searchable_item.clear_matches(cx);
624 }
625 }
626 if let Some(active_editor) = self.active_searchable_item.as_mut() {
627 self.selection_search_enabled = false;
628 self.replace_enabled = false;
629 active_editor.search_bar_visibility_changed(false, cx);
630 active_editor.toggle_filtered_search_ranges(false, cx);
631 let handle = active_editor.focus_handle(cx);
632 self.focus(&handle, cx);
633 }
634 cx.emit(Event::UpdateLocation);
635 cx.emit(ToolbarItemEvent::ChangeLocation(
636 ToolbarItemLocation::Hidden,
637 ));
638 cx.notify();
639 }
640
641 pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
642 if self.show(cx) {
643 if let Some(active_item) = self.active_searchable_item.as_mut() {
644 active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
645 }
646 self.search_suggested(cx);
647 self.smartcase(cx);
648 self.replace_enabled = deploy.replace_enabled;
649 self.selection_search_enabled = deploy.selection_search_enabled;
650 if deploy.focus {
651 let mut handle = self.query_editor.focus_handle(cx).clone();
652 let mut select_query = true;
653 if deploy.replace_enabled && handle.is_focused(cx) {
654 handle = self.replacement_editor.focus_handle(cx).clone();
655 select_query = false;
656 };
657
658 if select_query {
659 self.select_query(cx);
660 }
661
662 cx.focus(&handle);
663 }
664 return true;
665 }
666
667 cx.propagate();
668 false
669 }
670
671 pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
672 if self.is_dismissed() {
673 self.deploy(action, cx);
674 } else {
675 self.dismiss(&Dismiss, cx);
676 }
677 }
678
679 pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
680 let Some(handle) = self.active_searchable_item.as_ref() else {
681 return false;
682 };
683
684 self.configured_options =
685 SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
686 if self.dismissed && self.configured_options != self.default_options {
687 self.search_options = self.configured_options;
688 self.default_options = self.configured_options;
689 }
690
691 self.dismissed = false;
692 handle.search_bar_visibility_changed(true, cx);
693 cx.notify();
694 cx.emit(Event::UpdateLocation);
695 cx.emit(ToolbarItemEvent::ChangeLocation(
696 ToolbarItemLocation::Secondary,
697 ));
698 true
699 }
700
701 fn supported_options(&self) -> workspace::searchable::SearchOptions {
702 self.active_searchable_item
703 .as_deref()
704 .map(SearchableItemHandle::supported_options)
705 .unwrap_or_default()
706 }
707
708 pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
709 let search = self
710 .query_suggestion(cx)
711 .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
712
713 if let Some(search) = search {
714 cx.spawn(|this, mut cx| async move {
715 search.await?;
716 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
717 })
718 .detach_and_log_err(cx);
719 }
720 }
721
722 pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
723 if let Some(match_ix) = self.active_match_index {
724 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
725 if let Some(matches) = self
726 .searchable_items_with_matches
727 .get(&active_searchable_item.downgrade())
728 {
729 active_searchable_item.activate_match(match_ix, matches, cx)
730 }
731 }
732 }
733 }
734
735 pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
736 self.query_editor.update(cx, |query_editor, cx| {
737 query_editor.select_all(&Default::default(), cx);
738 });
739 }
740
741 pub fn query(&self, cx: &WindowContext) -> String {
742 self.query_editor.read(cx).text(cx)
743 }
744 pub fn replacement(&self, cx: &WindowContext) -> String {
745 self.replacement_editor.read(cx).text(cx)
746 }
747 pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
748 self.active_searchable_item
749 .as_ref()
750 .map(|searchable_item| searchable_item.query_suggestion(cx))
751 .filter(|suggestion| !suggestion.is_empty())
752 }
753
754 pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
755 if replacement.is_none() {
756 self.replace_enabled = false;
757 return;
758 }
759 self.replace_enabled = true;
760 self.replacement_editor
761 .update(cx, |replacement_editor, cx| {
762 replacement_editor
763 .buffer()
764 .update(cx, |replacement_buffer, cx| {
765 let len = replacement_buffer.len(cx);
766 replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
767 });
768 });
769 }
770
771 pub fn search(
772 &mut self,
773 query: &str,
774 options: Option<SearchOptions>,
775 cx: &mut ViewContext<Self>,
776 ) -> oneshot::Receiver<()> {
777 let options = options.unwrap_or(self.default_options);
778 let updated = query != self.query(cx) || self.search_options != options;
779 if updated {
780 self.query_editor.update(cx, |query_editor, cx| {
781 query_editor.buffer().update(cx, |query_buffer, cx| {
782 let len = query_buffer.len(cx);
783 query_buffer.edit([(0..len, query)], None, cx);
784 });
785 });
786 self.search_options = options;
787 self.clear_matches(cx);
788 cx.notify();
789 }
790 self.update_matches(!updated, cx)
791 }
792
793 fn render_search_option_button(
794 &self,
795 option: SearchOptions,
796 focus_handle: FocusHandle,
797 action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
798 ) -> impl IntoElement {
799 let is_active = self.search_options.contains(option);
800 option.as_button(is_active, focus_handle, action)
801 }
802
803 pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
804 if let Some(active_editor) = self.active_searchable_item.as_ref() {
805 let handle = active_editor.focus_handle(cx);
806 cx.focus(&handle);
807 }
808 }
809
810 pub fn toggle_search_option(
811 &mut self,
812 search_option: SearchOptions,
813 cx: &mut ViewContext<Self>,
814 ) {
815 self.search_options.toggle(search_option);
816 self.default_options = self.search_options;
817 drop(self.update_matches(false, cx));
818 cx.notify();
819 }
820
821 pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
822 self.search_options.contains(search_option)
823 }
824
825 pub fn enable_search_option(
826 &mut self,
827 search_option: SearchOptions,
828 cx: &mut ViewContext<Self>,
829 ) {
830 if !self.search_options.contains(search_option) {
831 self.toggle_search_option(search_option, cx)
832 }
833 }
834
835 pub fn set_search_options(
836 &mut self,
837 search_options: SearchOptions,
838 cx: &mut ViewContext<Self>,
839 ) {
840 self.search_options = search_options;
841 cx.notify();
842 }
843
844 fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
845 self.select_match(Direction::Next, 1, cx);
846 }
847
848 fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
849 self.select_match(Direction::Prev, 1, cx);
850 }
851
852 fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
853 if !self.dismissed && self.active_match_index.is_some() {
854 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
855 if let Some(matches) = self
856 .searchable_items_with_matches
857 .get(&searchable_item.downgrade())
858 {
859 searchable_item.select_matches(matches, cx);
860 self.focus_editor(&FocusEditor, cx);
861 }
862 }
863 }
864 }
865
866 pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
867 if let Some(index) = self.active_match_index {
868 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
869 if let Some(matches) = self
870 .searchable_items_with_matches
871 .get(&searchable_item.downgrade())
872 .filter(|matches| !matches.is_empty())
873 {
874 // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
875 if !EditorSettings::get_global(cx).search_wrap
876 && ((direction == Direction::Next && index + count >= matches.len())
877 || (direction == Direction::Prev && index < count))
878 {
879 crate::show_no_more_matches(cx);
880 return;
881 }
882 let new_match_index = searchable_item
883 .match_index_for_direction(matches, index, direction, count, cx);
884
885 searchable_item.update_matches(matches, cx);
886 searchable_item.activate_match(new_match_index, matches, cx);
887 }
888 }
889 }
890 }
891
892 pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
893 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
894 if let Some(matches) = self
895 .searchable_items_with_matches
896 .get(&searchable_item.downgrade())
897 {
898 if matches.is_empty() {
899 return;
900 }
901 let new_match_index = matches.len() - 1;
902 searchable_item.update_matches(matches, cx);
903 searchable_item.activate_match(new_match_index, matches, cx);
904 }
905 }
906 }
907
908 fn on_query_editor_event(
909 &mut self,
910 editor: View<Editor>,
911 event: &editor::EditorEvent,
912 cx: &mut ViewContext<Self>,
913 ) {
914 match event {
915 editor::EditorEvent::Focused => self.query_editor_focused = true,
916 editor::EditorEvent::Blurred => self.query_editor_focused = false,
917 editor::EditorEvent::Edited { .. } => {
918 self.smartcase(cx);
919 self.clear_matches(cx);
920 let search = self.update_matches(false, cx);
921
922 let width = editor.update(cx, |editor, cx| {
923 let text_layout_details = editor.text_layout_details(cx);
924 let snapshot = editor.snapshot(cx).display_snapshot;
925
926 snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
927 - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
928 });
929 self.editor_needed_width = width;
930 cx.notify();
931
932 cx.spawn(|this, mut cx| async move {
933 search.await?;
934 this.update(&mut cx, |this, cx| this.activate_current_match(cx))
935 })
936 .detach_and_log_err(cx);
937 }
938 _ => {}
939 }
940 }
941
942 fn on_replacement_editor_event(
943 &mut self,
944 _: View<Editor>,
945 event: &editor::EditorEvent,
946 _: &mut ViewContext<Self>,
947 ) {
948 match event {
949 editor::EditorEvent::Focused => self.replacement_editor_focused = true,
950 editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
951 _ => {}
952 }
953 }
954
955 fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
956 match event {
957 SearchEvent::MatchesInvalidated => {
958 drop(self.update_matches(false, cx));
959 }
960 SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
961 }
962 }
963
964 fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
965 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
966 }
967
968 fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
969 self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
970 }
971
972 fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
973 if let Some(active_item) = self.active_searchable_item.as_mut() {
974 self.selection_search_enabled = !self.selection_search_enabled;
975 active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
976 drop(self.update_matches(false, cx));
977 cx.notify();
978 }
979 }
980
981 fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
982 self.toggle_search_option(SearchOptions::REGEX, cx)
983 }
984
985 fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
986 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
987 self.active_match_index = None;
988 self.searchable_items_with_matches
989 .remove(&active_searchable_item.downgrade());
990 active_searchable_item.clear_matches(cx);
991 }
992 }
993
994 pub fn has_active_match(&self) -> bool {
995 self.active_match_index.is_some()
996 }
997
998 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
999 let mut active_item_matches = None;
1000 for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1001 if let Some(searchable_item) =
1002 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1003 {
1004 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1005 active_item_matches = Some((searchable_item.downgrade(), matches));
1006 } else {
1007 searchable_item.clear_matches(cx);
1008 }
1009 }
1010 }
1011
1012 self.searchable_items_with_matches
1013 .extend(active_item_matches);
1014 }
1015
1016 fn update_matches(
1017 &mut self,
1018 reuse_existing_query: bool,
1019 cx: &mut ViewContext<Self>,
1020 ) -> oneshot::Receiver<()> {
1021 let (done_tx, done_rx) = oneshot::channel();
1022 let query = self.query(cx);
1023 self.pending_search.take();
1024
1025 if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1026 self.query_contains_error = false;
1027 if query.is_empty() {
1028 self.clear_active_searchable_item_matches(cx);
1029 let _ = done_tx.send(());
1030 cx.notify();
1031 } else {
1032 let query: Arc<_> = if let Some(search) =
1033 self.active_search.take().filter(|_| reuse_existing_query)
1034 {
1035 search
1036 } else {
1037 if self.search_options.contains(SearchOptions::REGEX) {
1038 match SearchQuery::regex(
1039 query,
1040 self.search_options.contains(SearchOptions::WHOLE_WORD),
1041 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1042 false,
1043 Default::default(),
1044 Default::default(),
1045 None,
1046 ) {
1047 Ok(query) => query.with_replacement(self.replacement(cx)),
1048 Err(_) => {
1049 self.query_contains_error = true;
1050 self.clear_active_searchable_item_matches(cx);
1051 cx.notify();
1052 return done_rx;
1053 }
1054 }
1055 } else {
1056 match SearchQuery::text(
1057 query,
1058 self.search_options.contains(SearchOptions::WHOLE_WORD),
1059 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1060 false,
1061 Default::default(),
1062 Default::default(),
1063 None,
1064 ) {
1065 Ok(query) => query.with_replacement(self.replacement(cx)),
1066 Err(_) => {
1067 self.query_contains_error = true;
1068 self.clear_active_searchable_item_matches(cx);
1069 cx.notify();
1070 return done_rx;
1071 }
1072 }
1073 }
1074 .into()
1075 };
1076
1077 self.active_search = Some(query.clone());
1078 let query_text = query.as_str().to_string();
1079
1080 let matches = active_searchable_item.find_matches(query, cx);
1081
1082 let active_searchable_item = active_searchable_item.downgrade();
1083 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1084 let matches = matches.await;
1085
1086 this.update(&mut cx, |this, cx| {
1087 if let Some(active_searchable_item) =
1088 WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1089 {
1090 this.searchable_items_with_matches
1091 .insert(active_searchable_item.downgrade(), matches);
1092
1093 this.update_match_index(cx);
1094 this.search_history
1095 .add(&mut this.search_history_cursor, query_text);
1096 if !this.dismissed {
1097 let matches = this
1098 .searchable_items_with_matches
1099 .get(&active_searchable_item.downgrade())
1100 .unwrap();
1101 if matches.is_empty() {
1102 active_searchable_item.clear_matches(cx);
1103 } else {
1104 active_searchable_item.update_matches(matches, cx);
1105 }
1106 let _ = done_tx.send(());
1107 }
1108 cx.notify();
1109 }
1110 })
1111 .log_err();
1112 }));
1113 }
1114 }
1115 done_rx
1116 }
1117
1118 pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1119 let new_index = self
1120 .active_searchable_item
1121 .as_ref()
1122 .and_then(|searchable_item| {
1123 let matches = self
1124 .searchable_items_with_matches
1125 .get(&searchable_item.downgrade())?;
1126 searchable_item.active_match_index(matches, cx)
1127 });
1128 if new_index != self.active_match_index {
1129 self.active_match_index = new_index;
1130 cx.notify();
1131 }
1132 }
1133
1134 fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1135 // Search -> Replace -> Editor
1136 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1137 self.replacement_editor.focus_handle(cx)
1138 } else if let Some(item) = self.active_searchable_item.as_ref() {
1139 item.focus_handle(cx)
1140 } else {
1141 return;
1142 };
1143 self.focus(&focus_handle, cx);
1144 cx.stop_propagation();
1145 }
1146
1147 fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
1148 // Search -> Replace -> Search
1149 let focus_handle = if self.replace_enabled && self.query_editor_focused {
1150 self.replacement_editor.focus_handle(cx)
1151 } else if self.replacement_editor_focused {
1152 self.query_editor.focus_handle(cx)
1153 } else {
1154 return;
1155 };
1156 self.focus(&focus_handle, cx);
1157 cx.stop_propagation();
1158 }
1159
1160 fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1161 if let Some(new_query) = self
1162 .search_history
1163 .next(&mut self.search_history_cursor)
1164 .map(str::to_string)
1165 {
1166 drop(self.search(&new_query, Some(self.search_options), cx));
1167 } else {
1168 self.search_history_cursor.reset();
1169 drop(self.search("", Some(self.search_options), cx));
1170 }
1171 }
1172
1173 fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1174 if self.query(cx).is_empty() {
1175 if let Some(new_query) = self
1176 .search_history
1177 .current(&mut self.search_history_cursor)
1178 .map(str::to_string)
1179 {
1180 drop(self.search(&new_query, Some(self.search_options), cx));
1181 return;
1182 }
1183 }
1184
1185 if let Some(new_query) = self
1186 .search_history
1187 .previous(&mut self.search_history_cursor)
1188 .map(str::to_string)
1189 {
1190 drop(self.search(&new_query, Some(self.search_options), cx));
1191 }
1192 }
1193
1194 fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
1195 cx.on_next_frame(|_, cx| {
1196 cx.invalidate_character_coordinates();
1197 });
1198 cx.focus(handle);
1199 }
1200
1201 fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1202 if self.active_searchable_item.is_some() {
1203 self.replace_enabled = !self.replace_enabled;
1204 let handle = if self.replace_enabled {
1205 self.replacement_editor.focus_handle(cx)
1206 } else {
1207 self.query_editor.focus_handle(cx)
1208 };
1209 self.focus(&handle, cx);
1210 cx.notify();
1211 }
1212 }
1213
1214 fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1215 let mut should_propagate = true;
1216 if !self.dismissed && self.active_search.is_some() {
1217 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1218 if let Some(query) = self.active_search.as_ref() {
1219 if let Some(matches) = self
1220 .searchable_items_with_matches
1221 .get(&searchable_item.downgrade())
1222 {
1223 if let Some(active_index) = self.active_match_index {
1224 let query = query
1225 .as_ref()
1226 .clone()
1227 .with_replacement(self.replacement(cx));
1228 searchable_item.replace(matches.at(active_index), &query, cx);
1229 self.select_next_match(&SelectNextMatch, cx);
1230 }
1231 should_propagate = false;
1232 self.focus_editor(&FocusEditor, cx);
1233 }
1234 }
1235 }
1236 }
1237 if !should_propagate {
1238 cx.stop_propagation();
1239 }
1240 }
1241
1242 pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1243 if !self.dismissed && self.active_search.is_some() {
1244 if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1245 if let Some(query) = self.active_search.as_ref() {
1246 if let Some(matches) = self
1247 .searchable_items_with_matches
1248 .get(&searchable_item.downgrade())
1249 {
1250 let query = query
1251 .as_ref()
1252 .clone()
1253 .with_replacement(self.replacement(cx));
1254 searchable_item.replace_all(&mut matches.iter(), &query, cx);
1255 }
1256 }
1257 }
1258 }
1259 }
1260
1261 pub fn match_exists(&mut self, cx: &mut ViewContext<Self>) -> bool {
1262 self.update_match_index(cx);
1263 self.active_match_index.is_some()
1264 }
1265
1266 pub fn should_use_smartcase_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
1267 EditorSettings::get_global(cx).use_smartcase_search
1268 }
1269
1270 pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1271 str.chars().any(|c| c.is_uppercase())
1272 }
1273
1274 fn smartcase(&mut self, cx: &mut ViewContext<Self>) {
1275 if self.should_use_smartcase_search(cx) {
1276 let query = self.query(cx);
1277 if !query.is_empty() {
1278 let is_case = self.is_contains_uppercase(&query);
1279 if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1280 self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1281 }
1282 }
1283 }
1284 }
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289 use std::ops::Range;
1290
1291 use super::*;
1292 use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
1293 use gpui::{Context, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1294 use language::{Buffer, Point};
1295 use project::Project;
1296 use settings::SettingsStore;
1297 use smol::stream::StreamExt as _;
1298 use unindent::Unindent as _;
1299
1300 fn init_globals(cx: &mut TestAppContext) {
1301 cx.update(|cx| {
1302 let store = settings::SettingsStore::test(cx);
1303 cx.set_global(store);
1304 editor::init(cx);
1305
1306 language::init(cx);
1307 Project::init_settings(cx);
1308 theme::init(theme::LoadThemes::JustBase, cx);
1309 crate::init(cx);
1310 });
1311 }
1312
1313 fn init_test(
1314 cx: &mut TestAppContext,
1315 ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1316 init_globals(cx);
1317 let buffer = cx.new_model(|cx| {
1318 Buffer::local(
1319 r#"
1320 A regular expression (shortened as regex or regexp;[1] also referred to as
1321 rational expression[2][3]) is a sequence of characters that specifies a search
1322 pattern in text. Usually such patterns are used by string-searching algorithms
1323 for "find" or "find and replace" operations on strings, or for input validation.
1324 "#
1325 .unindent(),
1326 cx,
1327 )
1328 });
1329 let cx = cx.add_empty_window();
1330 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1331
1332 let search_bar = cx.new_view(|cx| {
1333 let mut search_bar = BufferSearchBar::new(cx);
1334 search_bar.set_active_pane_item(Some(&editor), cx);
1335 search_bar.show(cx);
1336 search_bar
1337 });
1338
1339 (editor, search_bar, cx)
1340 }
1341
1342 #[gpui::test]
1343 async fn test_search_simple(cx: &mut TestAppContext) {
1344 let (editor, search_bar, cx) = init_test(cx);
1345 let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1346 background_highlights
1347 .into_iter()
1348 .map(|(range, _)| range)
1349 .collect::<Vec<_>>()
1350 };
1351 // Search for a string that appears with different casing.
1352 // By default, search is case-insensitive.
1353 search_bar
1354 .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1355 .await
1356 .unwrap();
1357 editor.update(cx, |editor, cx| {
1358 assert_eq!(
1359 display_points_of(editor.all_text_background_highlights(cx)),
1360 &[
1361 DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1362 DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1363 ]
1364 );
1365 });
1366
1367 // Switch to a case sensitive search.
1368 search_bar.update(cx, |search_bar, cx| {
1369 search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1370 });
1371 let mut editor_notifications = cx.notifications(&editor);
1372 editor_notifications.next().await;
1373 editor.update(cx, |editor, cx| {
1374 assert_eq!(
1375 display_points_of(editor.all_text_background_highlights(cx)),
1376 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1377 );
1378 });
1379
1380 // Search for a string that appears both as a whole word and
1381 // within other words. By default, all results are found.
1382 search_bar
1383 .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1384 .await
1385 .unwrap();
1386 editor.update(cx, |editor, cx| {
1387 assert_eq!(
1388 display_points_of(editor.all_text_background_highlights(cx)),
1389 &[
1390 DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1391 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1392 DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1393 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1394 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1395 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1396 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1397 ]
1398 );
1399 });
1400
1401 // Switch to a whole word search.
1402 search_bar.update(cx, |search_bar, cx| {
1403 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1404 });
1405 let mut editor_notifications = cx.notifications(&editor);
1406 editor_notifications.next().await;
1407 editor.update(cx, |editor, cx| {
1408 assert_eq!(
1409 display_points_of(editor.all_text_background_highlights(cx)),
1410 &[
1411 DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1412 DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1413 DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1414 ]
1415 );
1416 });
1417
1418 editor.update(cx, |editor, cx| {
1419 editor.change_selections(None, cx, |s| {
1420 s.select_display_ranges([
1421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1422 ])
1423 });
1424 });
1425 search_bar.update(cx, |search_bar, cx| {
1426 assert_eq!(search_bar.active_match_index, Some(0));
1427 search_bar.select_next_match(&SelectNextMatch, cx);
1428 assert_eq!(
1429 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1430 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1431 );
1432 });
1433 search_bar.update(cx, |search_bar, _| {
1434 assert_eq!(search_bar.active_match_index, Some(0));
1435 });
1436
1437 search_bar.update(cx, |search_bar, cx| {
1438 search_bar.select_next_match(&SelectNextMatch, cx);
1439 assert_eq!(
1440 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1441 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1442 );
1443 });
1444 search_bar.update(cx, |search_bar, _| {
1445 assert_eq!(search_bar.active_match_index, Some(1));
1446 });
1447
1448 search_bar.update(cx, |search_bar, cx| {
1449 search_bar.select_next_match(&SelectNextMatch, cx);
1450 assert_eq!(
1451 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1452 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1453 );
1454 });
1455 search_bar.update(cx, |search_bar, _| {
1456 assert_eq!(search_bar.active_match_index, Some(2));
1457 });
1458
1459 search_bar.update(cx, |search_bar, cx| {
1460 search_bar.select_next_match(&SelectNextMatch, cx);
1461 assert_eq!(
1462 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1463 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1464 );
1465 });
1466 search_bar.update(cx, |search_bar, _| {
1467 assert_eq!(search_bar.active_match_index, Some(0));
1468 });
1469
1470 search_bar.update(cx, |search_bar, cx| {
1471 search_bar.select_prev_match(&SelectPrevMatch, cx);
1472 assert_eq!(
1473 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1474 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1475 );
1476 });
1477 search_bar.update(cx, |search_bar, _| {
1478 assert_eq!(search_bar.active_match_index, Some(2));
1479 });
1480
1481 search_bar.update(cx, |search_bar, cx| {
1482 search_bar.select_prev_match(&SelectPrevMatch, cx);
1483 assert_eq!(
1484 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1485 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1486 );
1487 });
1488 search_bar.update(cx, |search_bar, _| {
1489 assert_eq!(search_bar.active_match_index, Some(1));
1490 });
1491
1492 search_bar.update(cx, |search_bar, cx| {
1493 search_bar.select_prev_match(&SelectPrevMatch, cx);
1494 assert_eq!(
1495 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1496 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1497 );
1498 });
1499 search_bar.update(cx, |search_bar, _| {
1500 assert_eq!(search_bar.active_match_index, Some(0));
1501 });
1502
1503 // Park the cursor in between matches and ensure that going to the previous match selects
1504 // the closest match to the left.
1505 editor.update(cx, |editor, cx| {
1506 editor.change_selections(None, cx, |s| {
1507 s.select_display_ranges([
1508 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1509 ])
1510 });
1511 });
1512 search_bar.update(cx, |search_bar, cx| {
1513 assert_eq!(search_bar.active_match_index, Some(1));
1514 search_bar.select_prev_match(&SelectPrevMatch, cx);
1515 assert_eq!(
1516 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1517 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1518 );
1519 });
1520 search_bar.update(cx, |search_bar, _| {
1521 assert_eq!(search_bar.active_match_index, Some(0));
1522 });
1523
1524 // Park the cursor in between matches and ensure that going to the next match selects the
1525 // closest match to the right.
1526 editor.update(cx, |editor, cx| {
1527 editor.change_selections(None, cx, |s| {
1528 s.select_display_ranges([
1529 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1530 ])
1531 });
1532 });
1533 search_bar.update(cx, |search_bar, cx| {
1534 assert_eq!(search_bar.active_match_index, Some(1));
1535 search_bar.select_next_match(&SelectNextMatch, cx);
1536 assert_eq!(
1537 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1538 [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1539 );
1540 });
1541 search_bar.update(cx, |search_bar, _| {
1542 assert_eq!(search_bar.active_match_index, Some(1));
1543 });
1544
1545 // Park the cursor after the last match and ensure that going to the previous match selects
1546 // the last match.
1547 editor.update(cx, |editor, cx| {
1548 editor.change_selections(None, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1551 ])
1552 });
1553 });
1554 search_bar.update(cx, |search_bar, cx| {
1555 assert_eq!(search_bar.active_match_index, Some(2));
1556 search_bar.select_prev_match(&SelectPrevMatch, cx);
1557 assert_eq!(
1558 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1559 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1560 );
1561 });
1562 search_bar.update(cx, |search_bar, _| {
1563 assert_eq!(search_bar.active_match_index, Some(2));
1564 });
1565
1566 // Park the cursor after the last match and ensure that going to the next match selects the
1567 // first match.
1568 editor.update(cx, |editor, cx| {
1569 editor.change_selections(None, cx, |s| {
1570 s.select_display_ranges([
1571 DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1572 ])
1573 });
1574 });
1575 search_bar.update(cx, |search_bar, cx| {
1576 assert_eq!(search_bar.active_match_index, Some(2));
1577 search_bar.select_next_match(&SelectNextMatch, cx);
1578 assert_eq!(
1579 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1580 [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1581 );
1582 });
1583 search_bar.update(cx, |search_bar, _| {
1584 assert_eq!(search_bar.active_match_index, Some(0));
1585 });
1586
1587 // Park the cursor before the first match and ensure that going to the previous match
1588 // selects the last match.
1589 editor.update(cx, |editor, cx| {
1590 editor.change_selections(None, cx, |s| {
1591 s.select_display_ranges([
1592 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1593 ])
1594 });
1595 });
1596 search_bar.update(cx, |search_bar, cx| {
1597 assert_eq!(search_bar.active_match_index, Some(0));
1598 search_bar.select_prev_match(&SelectPrevMatch, cx);
1599 assert_eq!(
1600 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1601 [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1602 );
1603 });
1604 search_bar.update(cx, |search_bar, _| {
1605 assert_eq!(search_bar.active_match_index, Some(2));
1606 });
1607 }
1608
1609 fn display_points_of(
1610 background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1611 ) -> Vec<Range<DisplayPoint>> {
1612 background_highlights
1613 .into_iter()
1614 .map(|(range, _)| range)
1615 .collect::<Vec<_>>()
1616 }
1617
1618 #[gpui::test]
1619 async fn test_search_option_handling(cx: &mut TestAppContext) {
1620 let (editor, search_bar, cx) = init_test(cx);
1621
1622 // show with options should make current search case sensitive
1623 search_bar
1624 .update(cx, |search_bar, cx| {
1625 search_bar.show(cx);
1626 search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1627 })
1628 .await
1629 .unwrap();
1630 editor.update(cx, |editor, cx| {
1631 assert_eq!(
1632 display_points_of(editor.all_text_background_highlights(cx)),
1633 &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1634 );
1635 });
1636
1637 // search_suggested should restore default options
1638 search_bar.update(cx, |search_bar, cx| {
1639 search_bar.search_suggested(cx);
1640 assert_eq!(search_bar.search_options, SearchOptions::NONE)
1641 });
1642
1643 // toggling a search option should update the defaults
1644 search_bar
1645 .update(cx, |search_bar, cx| {
1646 search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1647 })
1648 .await
1649 .unwrap();
1650 search_bar.update(cx, |search_bar, cx| {
1651 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1652 });
1653 let mut editor_notifications = cx.notifications(&editor);
1654 editor_notifications.next().await;
1655 editor.update(cx, |editor, cx| {
1656 assert_eq!(
1657 display_points_of(editor.all_text_background_highlights(cx)),
1658 &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1659 );
1660 });
1661
1662 // defaults should still include whole word
1663 search_bar.update(cx, |search_bar, cx| {
1664 search_bar.search_suggested(cx);
1665 assert_eq!(
1666 search_bar.search_options,
1667 SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1668 )
1669 });
1670 }
1671
1672 #[gpui::test]
1673 async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1674 init_globals(cx);
1675 let buffer_text = r#"
1676 A regular expression (shortened as regex or regexp;[1] also referred to as
1677 rational expression[2][3]) is a sequence of characters that specifies a search
1678 pattern in text. Usually such patterns are used by string-searching algorithms
1679 for "find" or "find and replace" operations on strings, or for input validation.
1680 "#
1681 .unindent();
1682 let expected_query_matches_count = buffer_text
1683 .chars()
1684 .filter(|c| c.to_ascii_lowercase() == 'a')
1685 .count();
1686 assert!(
1687 expected_query_matches_count > 1,
1688 "Should pick a query with multiple results"
1689 );
1690 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1691 let window = cx.add_window(|_| gpui::Empty);
1692
1693 let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1694
1695 let search_bar = window.build_view(cx, |cx| {
1696 let mut search_bar = BufferSearchBar::new(cx);
1697 search_bar.set_active_pane_item(Some(&editor), cx);
1698 search_bar.show(cx);
1699 search_bar
1700 });
1701
1702 window
1703 .update(cx, |_, cx| {
1704 search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1705 })
1706 .unwrap()
1707 .await
1708 .unwrap();
1709 let initial_selections = window
1710 .update(cx, |_, cx| {
1711 search_bar.update(cx, |search_bar, cx| {
1712 let handle = search_bar.query_editor.focus_handle(cx);
1713 cx.focus(&handle);
1714 search_bar.activate_current_match(cx);
1715 });
1716 assert!(
1717 !editor.read(cx).is_focused(cx),
1718 "Initially, the editor should not be focused"
1719 );
1720 let initial_selections = editor.update(cx, |editor, cx| {
1721 let initial_selections = editor.selections.display_ranges(cx);
1722 assert_eq!(
1723 initial_selections.len(), 1,
1724 "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1725 );
1726 initial_selections
1727 });
1728 search_bar.update(cx, |search_bar, cx| {
1729 assert_eq!(search_bar.active_match_index, Some(0));
1730 let handle = search_bar.query_editor.focus_handle(cx);
1731 cx.focus(&handle);
1732 search_bar.select_all_matches(&SelectAllMatches, cx);
1733 });
1734 assert!(
1735 editor.read(cx).is_focused(cx),
1736 "Should focus editor after successful SelectAllMatches"
1737 );
1738 search_bar.update(cx, |search_bar, cx| {
1739 let all_selections =
1740 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1741 assert_eq!(
1742 all_selections.len(),
1743 expected_query_matches_count,
1744 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1745 );
1746 assert_eq!(
1747 search_bar.active_match_index,
1748 Some(0),
1749 "Match index should not change after selecting all matches"
1750 );
1751 });
1752
1753 search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1754 initial_selections
1755 }).unwrap();
1756
1757 window
1758 .update(cx, |_, cx| {
1759 assert!(
1760 editor.read(cx).is_focused(cx),
1761 "Should still have editor focused after SelectNextMatch"
1762 );
1763 search_bar.update(cx, |search_bar, cx| {
1764 let all_selections =
1765 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1766 assert_eq!(
1767 all_selections.len(),
1768 1,
1769 "On next match, should deselect items and select the next match"
1770 );
1771 assert_ne!(
1772 all_selections, initial_selections,
1773 "Next match should be different from the first selection"
1774 );
1775 assert_eq!(
1776 search_bar.active_match_index,
1777 Some(1),
1778 "Match index should be updated to the next one"
1779 );
1780 let handle = search_bar.query_editor.focus_handle(cx);
1781 cx.focus(&handle);
1782 search_bar.select_all_matches(&SelectAllMatches, cx);
1783 });
1784 })
1785 .unwrap();
1786 window
1787 .update(cx, |_, cx| {
1788 assert!(
1789 editor.read(cx).is_focused(cx),
1790 "Should focus editor after successful SelectAllMatches"
1791 );
1792 search_bar.update(cx, |search_bar, cx| {
1793 let all_selections =
1794 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1795 assert_eq!(
1796 all_selections.len(),
1797 expected_query_matches_count,
1798 "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1799 );
1800 assert_eq!(
1801 search_bar.active_match_index,
1802 Some(1),
1803 "Match index should not change after selecting all matches"
1804 );
1805 });
1806 search_bar.update(cx, |search_bar, cx| {
1807 search_bar.select_prev_match(&SelectPrevMatch, cx);
1808 });
1809 })
1810 .unwrap();
1811 let last_match_selections = window
1812 .update(cx, |_, cx| {
1813 assert!(
1814 editor.read(cx).is_focused(cx),
1815 "Should still have editor focused after SelectPrevMatch"
1816 );
1817
1818 search_bar.update(cx, |search_bar, cx| {
1819 let all_selections =
1820 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1821 assert_eq!(
1822 all_selections.len(),
1823 1,
1824 "On previous match, should deselect items and select the previous item"
1825 );
1826 assert_eq!(
1827 all_selections, initial_selections,
1828 "Previous match should be the same as the first selection"
1829 );
1830 assert_eq!(
1831 search_bar.active_match_index,
1832 Some(0),
1833 "Match index should be updated to the previous one"
1834 );
1835 all_selections
1836 })
1837 })
1838 .unwrap();
1839
1840 window
1841 .update(cx, |_, cx| {
1842 search_bar.update(cx, |search_bar, cx| {
1843 let handle = search_bar.query_editor.focus_handle(cx);
1844 cx.focus(&handle);
1845 search_bar.search("abas_nonexistent_match", None, cx)
1846 })
1847 })
1848 .unwrap()
1849 .await
1850 .unwrap();
1851 window
1852 .update(cx, |_, cx| {
1853 search_bar.update(cx, |search_bar, cx| {
1854 search_bar.select_all_matches(&SelectAllMatches, cx);
1855 });
1856 assert!(
1857 editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1858 "Should not switch focus to editor if SelectAllMatches does not find any matches"
1859 );
1860 search_bar.update(cx, |search_bar, cx| {
1861 let all_selections =
1862 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1863 assert_eq!(
1864 all_selections, last_match_selections,
1865 "Should not select anything new if there are no matches"
1866 );
1867 assert!(
1868 search_bar.active_match_index.is_none(),
1869 "For no matches, there should be no active match index"
1870 );
1871 });
1872 })
1873 .unwrap();
1874 }
1875
1876 #[gpui::test]
1877 async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
1878 init_globals(cx);
1879 let buffer_text = r#"
1880 self.buffer.update(cx, |buffer, cx| {
1881 buffer.edit(
1882 edits,
1883 Some(AutoindentMode::Block {
1884 original_indent_columns,
1885 }),
1886 cx,
1887 )
1888 });
1889
1890 this.buffer.update(cx, |buffer, cx| {
1891 buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
1892 });
1893 "#
1894 .unindent();
1895 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1896 let cx = cx.add_empty_window();
1897
1898 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1899
1900 let search_bar = cx.new_view(|cx| {
1901 let mut search_bar = BufferSearchBar::new(cx);
1902 search_bar.set_active_pane_item(Some(&editor), cx);
1903 search_bar.show(cx);
1904 search_bar
1905 });
1906
1907 search_bar
1908 .update(cx, |search_bar, cx| {
1909 search_bar.search(
1910 "edit\\(",
1911 Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
1912 cx,
1913 )
1914 })
1915 .await
1916 .unwrap();
1917
1918 search_bar.update(cx, |search_bar, cx| {
1919 search_bar.select_all_matches(&SelectAllMatches, cx);
1920 });
1921 search_bar.update(cx, |_, cx| {
1922 let all_selections =
1923 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1924 assert_eq!(
1925 all_selections.len(),
1926 2,
1927 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
1928 );
1929 });
1930
1931 search_bar
1932 .update(cx, |search_bar, cx| {
1933 search_bar.search(
1934 "edit(",
1935 Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
1936 cx,
1937 )
1938 })
1939 .await
1940 .unwrap();
1941
1942 search_bar.update(cx, |search_bar, cx| {
1943 search_bar.select_all_matches(&SelectAllMatches, cx);
1944 });
1945 search_bar.update(cx, |_, cx| {
1946 let all_selections =
1947 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1948 assert_eq!(
1949 all_selections.len(),
1950 2,
1951 "Should select all `edit(` in the buffer, but got: {all_selections:?}"
1952 );
1953 });
1954 }
1955
1956 #[gpui::test]
1957 async fn test_search_query_history(cx: &mut TestAppContext) {
1958 init_globals(cx);
1959 let buffer_text = r#"
1960 A regular expression (shortened as regex or regexp;[1] also referred to as
1961 rational expression[2][3]) is a sequence of characters that specifies a search
1962 pattern in text. Usually such patterns are used by string-searching algorithms
1963 for "find" or "find and replace" operations on strings, or for input validation.
1964 "#
1965 .unindent();
1966 let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx));
1967 let cx = cx.add_empty_window();
1968
1969 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1970
1971 let search_bar = cx.new_view(|cx| {
1972 let mut search_bar = BufferSearchBar::new(cx);
1973 search_bar.set_active_pane_item(Some(&editor), cx);
1974 search_bar.show(cx);
1975 search_bar
1976 });
1977
1978 // Add 3 search items into the history.
1979 search_bar
1980 .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1981 .await
1982 .unwrap();
1983 search_bar
1984 .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1985 .await
1986 .unwrap();
1987 search_bar
1988 .update(cx, |search_bar, cx| {
1989 search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1990 })
1991 .await
1992 .unwrap();
1993 // Ensure that the latest search is active.
1994 search_bar.update(cx, |search_bar, cx| {
1995 assert_eq!(search_bar.query(cx), "c");
1996 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1997 });
1998
1999 // Next history query after the latest should set the query to the empty string.
2000 search_bar.update(cx, |search_bar, cx| {
2001 search_bar.next_history_query(&NextHistoryQuery, cx);
2002 });
2003 search_bar.update(cx, |search_bar, cx| {
2004 assert_eq!(search_bar.query(cx), "");
2005 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2006 });
2007 search_bar.update(cx, |search_bar, cx| {
2008 search_bar.next_history_query(&NextHistoryQuery, cx);
2009 });
2010 search_bar.update(cx, |search_bar, cx| {
2011 assert_eq!(search_bar.query(cx), "");
2012 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2013 });
2014
2015 // First previous query for empty current query should set the query to the latest.
2016 search_bar.update(cx, |search_bar, cx| {
2017 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2018 });
2019 search_bar.update(cx, |search_bar, cx| {
2020 assert_eq!(search_bar.query(cx), "c");
2021 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2022 });
2023
2024 // Further previous items should go over the history in reverse order.
2025 search_bar.update(cx, |search_bar, cx| {
2026 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2027 });
2028 search_bar.update(cx, |search_bar, cx| {
2029 assert_eq!(search_bar.query(cx), "b");
2030 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2031 });
2032
2033 // Previous items should never go behind the first history item.
2034 search_bar.update(cx, |search_bar, cx| {
2035 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2036 });
2037 search_bar.update(cx, |search_bar, cx| {
2038 assert_eq!(search_bar.query(cx), "a");
2039 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2040 });
2041 search_bar.update(cx, |search_bar, cx| {
2042 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2043 });
2044 search_bar.update(cx, |search_bar, cx| {
2045 assert_eq!(search_bar.query(cx), "a");
2046 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2047 });
2048
2049 // Next items should go over the history in the original order.
2050 search_bar.update(cx, |search_bar, cx| {
2051 search_bar.next_history_query(&NextHistoryQuery, cx);
2052 });
2053 search_bar.update(cx, |search_bar, cx| {
2054 assert_eq!(search_bar.query(cx), "b");
2055 assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2056 });
2057
2058 search_bar
2059 .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
2060 .await
2061 .unwrap();
2062 search_bar.update(cx, |search_bar, cx| {
2063 assert_eq!(search_bar.query(cx), "ba");
2064 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2065 });
2066
2067 // New search input should add another entry to history and move the selection to the end of the history.
2068 search_bar.update(cx, |search_bar, cx| {
2069 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2070 });
2071 search_bar.update(cx, |search_bar, cx| {
2072 assert_eq!(search_bar.query(cx), "c");
2073 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2074 });
2075 search_bar.update(cx, |search_bar, cx| {
2076 search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2077 });
2078 search_bar.update(cx, |search_bar, cx| {
2079 assert_eq!(search_bar.query(cx), "b");
2080 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2081 });
2082 search_bar.update(cx, |search_bar, cx| {
2083 search_bar.next_history_query(&NextHistoryQuery, cx);
2084 });
2085 search_bar.update(cx, |search_bar, cx| {
2086 assert_eq!(search_bar.query(cx), "c");
2087 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2088 });
2089 search_bar.update(cx, |search_bar, cx| {
2090 search_bar.next_history_query(&NextHistoryQuery, cx);
2091 });
2092 search_bar.update(cx, |search_bar, cx| {
2093 assert_eq!(search_bar.query(cx), "ba");
2094 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2095 });
2096 search_bar.update(cx, |search_bar, cx| {
2097 search_bar.next_history_query(&NextHistoryQuery, cx);
2098 });
2099 search_bar.update(cx, |search_bar, cx| {
2100 assert_eq!(search_bar.query(cx), "");
2101 assert_eq!(search_bar.search_options, SearchOptions::NONE);
2102 });
2103 }
2104
2105 #[gpui::test]
2106 async fn test_replace_simple(cx: &mut TestAppContext) {
2107 let (editor, search_bar, cx) = init_test(cx);
2108
2109 search_bar
2110 .update(cx, |search_bar, cx| {
2111 search_bar.search("expression", None, cx)
2112 })
2113 .await
2114 .unwrap();
2115
2116 search_bar.update(cx, |search_bar, cx| {
2117 search_bar.replacement_editor.update(cx, |editor, cx| {
2118 // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2119 editor.set_text("expr$1", cx);
2120 });
2121 search_bar.replace_all(&ReplaceAll, cx)
2122 });
2123 assert_eq!(
2124 editor.update(cx, |this, cx| { this.text(cx) }),
2125 r#"
2126 A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2127 rational expr$1[2][3]) is a sequence of characters that specifies a search
2128 pattern in text. Usually such patterns are used by string-searching algorithms
2129 for "find" or "find and replace" operations on strings, or for input validation.
2130 "#
2131 .unindent()
2132 );
2133
2134 // Search for word boundaries and replace just a single one.
2135 search_bar
2136 .update(cx, |search_bar, cx| {
2137 search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
2138 })
2139 .await
2140 .unwrap();
2141
2142 search_bar.update(cx, |search_bar, cx| {
2143 search_bar.replacement_editor.update(cx, |editor, cx| {
2144 editor.set_text("banana", cx);
2145 });
2146 search_bar.replace_next(&ReplaceNext, cx)
2147 });
2148 // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2149 assert_eq!(
2150 editor.update(cx, |this, cx| { this.text(cx) }),
2151 r#"
2152 A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2153 rational expr$1[2][3]) is a sequence of characters that specifies a search
2154 pattern in text. Usually such patterns are used by string-searching algorithms
2155 for "find" or "find and replace" operations on strings, or for input validation.
2156 "#
2157 .unindent()
2158 );
2159 // Let's turn on regex mode.
2160 search_bar
2161 .update(cx, |search_bar, cx| {
2162 search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
2163 })
2164 .await
2165 .unwrap();
2166 search_bar.update(cx, |search_bar, cx| {
2167 search_bar.replacement_editor.update(cx, |editor, cx| {
2168 editor.set_text("${1}number", cx);
2169 });
2170 search_bar.replace_all(&ReplaceAll, cx)
2171 });
2172 assert_eq!(
2173 editor.update(cx, |this, cx| { this.text(cx) }),
2174 r#"
2175 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2176 rational expr$12number3number) is a sequence of characters that specifies a search
2177 pattern in text. Usually such patterns are used by string-searching algorithms
2178 for "find" or "find and replace" operations on strings, or for input validation.
2179 "#
2180 .unindent()
2181 );
2182 // Now with a whole-word twist.
2183 search_bar
2184 .update(cx, |search_bar, cx| {
2185 search_bar.search(
2186 "a\\w+s",
2187 Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2188 cx,
2189 )
2190 })
2191 .await
2192 .unwrap();
2193 search_bar.update(cx, |search_bar, cx| {
2194 search_bar.replacement_editor.update(cx, |editor, cx| {
2195 editor.set_text("things", cx);
2196 });
2197 search_bar.replace_all(&ReplaceAll, cx)
2198 });
2199 // The only word affected by this edit should be `algorithms`, even though there's a bunch
2200 // of words in this text that would match this regex if not for WHOLE_WORD.
2201 assert_eq!(
2202 editor.update(cx, |this, cx| { this.text(cx) }),
2203 r#"
2204 A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2205 rational expr$12number3number) is a sequence of characters that specifies a search
2206 pattern in text. Usually such patterns are used by string-searching things
2207 for "find" or "find and replace" operations on strings, or for input validation.
2208 "#
2209 .unindent()
2210 );
2211 }
2212
2213 struct ReplacementTestParams<'a> {
2214 editor: &'a View<Editor>,
2215 search_bar: &'a View<BufferSearchBar>,
2216 cx: &'a mut VisualTestContext,
2217 search_text: &'static str,
2218 search_options: Option<SearchOptions>,
2219 replacement_text: &'static str,
2220 replace_all: bool,
2221 expected_text: String,
2222 }
2223
2224 async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2225 options
2226 .search_bar
2227 .update(options.cx, |search_bar, cx| {
2228 if let Some(options) = options.search_options {
2229 search_bar.set_search_options(options, cx);
2230 }
2231 search_bar.search(options.search_text, options.search_options, cx)
2232 })
2233 .await
2234 .unwrap();
2235
2236 options.search_bar.update(options.cx, |search_bar, cx| {
2237 search_bar.replacement_editor.update(cx, |editor, cx| {
2238 editor.set_text(options.replacement_text, cx);
2239 });
2240
2241 if options.replace_all {
2242 search_bar.replace_all(&ReplaceAll, cx)
2243 } else {
2244 search_bar.replace_next(&ReplaceNext, cx)
2245 }
2246 });
2247
2248 assert_eq!(
2249 options
2250 .editor
2251 .update(options.cx, |this, cx| { this.text(cx) }),
2252 options.expected_text
2253 );
2254 }
2255
2256 #[gpui::test]
2257 async fn test_replace_special_characters(cx: &mut TestAppContext) {
2258 let (editor, search_bar, cx) = init_test(cx);
2259
2260 run_replacement_test(ReplacementTestParams {
2261 editor: &editor,
2262 search_bar: &search_bar,
2263 cx,
2264 search_text: "expression",
2265 search_options: None,
2266 replacement_text: r"\n",
2267 replace_all: true,
2268 expected_text: r#"
2269 A regular \n (shortened as regex or regexp;[1] also referred to as
2270 rational \n[2][3]) is a sequence of characters that specifies a search
2271 pattern in text. Usually such patterns are used by string-searching algorithms
2272 for "find" or "find and replace" operations on strings, or for input validation.
2273 "#
2274 .unindent(),
2275 })
2276 .await;
2277
2278 run_replacement_test(ReplacementTestParams {
2279 editor: &editor,
2280 search_bar: &search_bar,
2281 cx,
2282 search_text: "or",
2283 search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2284 replacement_text: r"\\\n\\\\",
2285 replace_all: false,
2286 expected_text: r#"
2287 A regular \n (shortened as regex \
2288 \\ regexp;[1] also referred to as
2289 rational \n[2][3]) is a sequence of characters that specifies a search
2290 pattern in text. Usually such patterns are used by string-searching algorithms
2291 for "find" or "find and replace" operations on strings, or for input validation.
2292 "#
2293 .unindent(),
2294 })
2295 .await;
2296
2297 run_replacement_test(ReplacementTestParams {
2298 editor: &editor,
2299 search_bar: &search_bar,
2300 cx,
2301 search_text: r"(that|used) ",
2302 search_options: Some(SearchOptions::REGEX),
2303 replacement_text: r"$1\n",
2304 replace_all: true,
2305 expected_text: r#"
2306 A regular \n (shortened as regex \
2307 \\ regexp;[1] also referred to as
2308 rational \n[2][3]) is a sequence of characters that
2309 specifies a search
2310 pattern in text. Usually such patterns are used
2311 by string-searching algorithms
2312 for "find" or "find and replace" operations on strings, or for input validation.
2313 "#
2314 .unindent(),
2315 })
2316 .await;
2317 }
2318
2319 #[gpui::test]
2320 async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2321 cx: &mut TestAppContext,
2322 ) {
2323 init_globals(cx);
2324 let buffer = cx.new_model(|cx| {
2325 Buffer::local(
2326 r#"
2327 aaa bbb aaa ccc
2328 aaa bbb aaa ccc
2329 aaa bbb aaa ccc
2330 aaa bbb aaa ccc
2331 aaa bbb aaa ccc
2332 aaa bbb aaa ccc
2333 "#
2334 .unindent(),
2335 cx,
2336 )
2337 });
2338 let cx = cx.add_empty_window();
2339 let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
2340
2341 let search_bar = cx.new_view(|cx| {
2342 let mut search_bar = BufferSearchBar::new(cx);
2343 search_bar.set_active_pane_item(Some(&editor), cx);
2344 search_bar.show(cx);
2345 search_bar
2346 });
2347
2348 editor.update(cx, |editor, cx| {
2349 editor.change_selections(None, cx, |s| {
2350 s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2351 })
2352 });
2353
2354 search_bar.update(cx, |search_bar, cx| {
2355 let deploy = Deploy {
2356 focus: true,
2357 replace_enabled: false,
2358 selection_search_enabled: true,
2359 };
2360 search_bar.deploy(&deploy, cx);
2361 });
2362
2363 cx.run_until_parked();
2364
2365 search_bar
2366 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2367 .await
2368 .unwrap();
2369
2370 editor.update(cx, |editor, cx| {
2371 assert_eq!(
2372 editor.search_background_highlights(cx),
2373 &[
2374 Point::new(1, 0)..Point::new(1, 3),
2375 Point::new(1, 8)..Point::new(1, 11),
2376 Point::new(2, 0)..Point::new(2, 3),
2377 ]
2378 );
2379 });
2380 }
2381
2382 #[gpui::test]
2383 async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2384 cx: &mut TestAppContext,
2385 ) {
2386 init_globals(cx);
2387 let text = r#"
2388 aaa bbb aaa ccc
2389 aaa bbb aaa ccc
2390 aaa bbb aaa ccc
2391 aaa bbb aaa ccc
2392 aaa bbb aaa ccc
2393 aaa bbb aaa ccc
2394
2395 aaa bbb aaa ccc
2396 aaa bbb aaa ccc
2397 aaa bbb aaa ccc
2398 aaa bbb aaa ccc
2399 aaa bbb aaa ccc
2400 aaa bbb aaa ccc
2401 "#
2402 .unindent();
2403
2404 let cx = cx.add_empty_window();
2405 let editor = cx.new_view(|cx| {
2406 let multibuffer = MultiBuffer::build_multi(
2407 [
2408 (
2409 &text,
2410 vec![
2411 Point::new(0, 0)..Point::new(2, 0),
2412 Point::new(4, 0)..Point::new(5, 0),
2413 ],
2414 ),
2415 (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2416 ],
2417 cx,
2418 );
2419 Editor::for_multibuffer(multibuffer, None, false, cx)
2420 });
2421
2422 let search_bar = cx.new_view(|cx| {
2423 let mut search_bar = BufferSearchBar::new(cx);
2424 search_bar.set_active_pane_item(Some(&editor), cx);
2425 search_bar.show(cx);
2426 search_bar
2427 });
2428
2429 editor.update(cx, |editor, cx| {
2430 editor.change_selections(None, cx, |s| {
2431 s.select_ranges(vec![
2432 Point::new(1, 0)..Point::new(1, 4),
2433 Point::new(5, 3)..Point::new(6, 4),
2434 ])
2435 })
2436 });
2437
2438 search_bar.update(cx, |search_bar, cx| {
2439 let deploy = Deploy {
2440 focus: true,
2441 replace_enabled: false,
2442 selection_search_enabled: true,
2443 };
2444 search_bar.deploy(&deploy, cx);
2445 });
2446
2447 cx.run_until_parked();
2448
2449 search_bar
2450 .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
2451 .await
2452 .unwrap();
2453
2454 editor.update(cx, |editor, cx| {
2455 assert_eq!(
2456 editor.search_background_highlights(cx),
2457 &[
2458 Point::new(1, 0)..Point::new(1, 3),
2459 Point::new(5, 8)..Point::new(5, 11),
2460 Point::new(6, 0)..Point::new(6, 3),
2461 ]
2462 );
2463 });
2464 }
2465
2466 #[gpui::test]
2467 async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2468 let (editor, search_bar, cx) = init_test(cx);
2469 // Search using valid regexp
2470 search_bar
2471 .update(cx, |search_bar, cx| {
2472 search_bar.enable_search_option(SearchOptions::REGEX, cx);
2473 search_bar.search("expression", None, cx)
2474 })
2475 .await
2476 .unwrap();
2477 editor.update(cx, |editor, cx| {
2478 assert_eq!(
2479 display_points_of(editor.all_text_background_highlights(cx)),
2480 &[
2481 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2482 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2483 ],
2484 );
2485 });
2486
2487 // Now, the expression is invalid
2488 search_bar
2489 .update(cx, |search_bar, cx| {
2490 search_bar.search("expression (", None, cx)
2491 })
2492 .await
2493 .unwrap_err();
2494 editor.update(cx, |editor, cx| {
2495 assert!(display_points_of(editor.all_text_background_highlights(cx)).is_empty(),);
2496 });
2497 }
2498
2499 #[gpui::test]
2500 async fn test_search_options_changes(cx: &mut TestAppContext) {
2501 let (_editor, search_bar, cx) = init_test(cx);
2502 update_search_settings(
2503 SearchSettings {
2504 whole_word: false,
2505 case_sensitive: false,
2506 include_ignored: false,
2507 regex: false,
2508 },
2509 cx,
2510 );
2511
2512 let deploy = Deploy {
2513 focus: true,
2514 replace_enabled: false,
2515 selection_search_enabled: true,
2516 };
2517
2518 search_bar.update(cx, |search_bar, cx| {
2519 assert_eq!(
2520 search_bar.search_options,
2521 SearchOptions::NONE,
2522 "Should have no search options enabled by default"
2523 );
2524 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2525 assert_eq!(
2526 search_bar.search_options,
2527 SearchOptions::WHOLE_WORD,
2528 "Should enable the option toggled"
2529 );
2530 assert!(
2531 !search_bar.dismissed,
2532 "Search bar should be present and visible"
2533 );
2534 search_bar.deploy(&deploy, cx);
2535 assert_eq!(
2536 search_bar.configured_options,
2537 SearchOptions::NONE,
2538 "Should have configured search options matching the settings"
2539 );
2540 assert_eq!(
2541 search_bar.search_options,
2542 SearchOptions::WHOLE_WORD,
2543 "After (re)deploying, the option should still be enabled"
2544 );
2545
2546 search_bar.dismiss(&Dismiss, cx);
2547 search_bar.deploy(&deploy, cx);
2548 assert_eq!(
2549 search_bar.search_options,
2550 SearchOptions::NONE,
2551 "After hiding and showing the search bar, default options should be used"
2552 );
2553
2554 search_bar.toggle_search_option(SearchOptions::REGEX, cx);
2555 search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
2556 assert_eq!(
2557 search_bar.search_options,
2558 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2559 "Should enable the options toggled"
2560 );
2561 assert!(
2562 !search_bar.dismissed,
2563 "Search bar should be present and visible"
2564 );
2565 });
2566
2567 update_search_settings(
2568 SearchSettings {
2569 whole_word: false,
2570 case_sensitive: true,
2571 include_ignored: false,
2572 regex: false,
2573 },
2574 cx,
2575 );
2576 search_bar.update(cx, |search_bar, cx| {
2577 assert_eq!(
2578 search_bar.search_options,
2579 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2580 "Should have no search options enabled by default"
2581 );
2582
2583 search_bar.deploy(&deploy, cx);
2584 assert_eq!(
2585 search_bar.configured_options,
2586 SearchOptions::CASE_SENSITIVE,
2587 "Should have configured search options matching the settings"
2588 );
2589 assert_eq!(
2590 search_bar.search_options,
2591 SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2592 "Toggling a non-dismissed search bar with custom options should not change the default options"
2593 );
2594 search_bar.dismiss(&Dismiss, cx);
2595 search_bar.deploy(&deploy, cx);
2596 assert_eq!(
2597 search_bar.search_options,
2598 SearchOptions::CASE_SENSITIVE,
2599 "After hiding and showing the search bar, default options should be used"
2600 );
2601 });
2602 }
2603
2604 fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2605 cx.update(|cx| {
2606 SettingsStore::update_global(cx, |store, cx| {
2607 store.update_user_settings::<EditorSettings>(cx, |settings| {
2608 settings.search = Some(search_settings);
2609 });
2610 });
2611 });
2612 }
2613}