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