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