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