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