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