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