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