Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:57:23.714Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

buffer_search.rs

4236 lines · 162.4 KB · rust
1mod registrar;
2
3use crate::{
4    FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
5    SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
6    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
7    buffer_search::registrar::WithResultsOrExternalQuery,
8    search_bar::{
9        ActionButtonState, HistoryNavigationDirection, alignment_element,
10        filter_search_results_input, input_base_styles, render_action_button, render_text_input,
11        should_navigate_history,
12    },
13};
14use any_vec::AnyVec;
15use collections::HashMap;
16use editor::{
17    DiffStyleControls, Editor, EditorSettings, MultiBufferOffset, SplittableEditor,
18    actions::{Backtab, FoldAll, Tab, ToggleFoldAll, ToggleSoftWrap, UnfoldAll},
19    scroll::Autoscroll,
20};
21use futures::channel::oneshot;
22use gpui::{
23    App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
24    IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
25    TaskExt, WeakEntity, Window, div,
26};
27use language::{Language, LanguageRegistry};
28use project::{
29    search::SearchQuery,
30    search_history::{SearchHistory, SearchHistoryCursor},
31};
32
33use settings::{SeedQuerySetting, Settings};
34use std::{any::TypeId, sync::Arc};
35use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
36
37use ui::{BASE_REM_SIZE_IN_PX, IconButtonShape, Tooltip, prelude::*, utils::SearchInputWidth};
38use util::{ResultExt, paths::PathMatcher};
39use workspace::{
40    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
41    item::{ItemBufferKind, ItemHandle},
42    searchable::{
43        Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItemHandle,
44        WeakSearchableItemHandle,
45    },
46};
47
48pub use registrar::{DivRegistrar, register_pane_search_actions};
49use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};
50
51const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
52
53pub use zed_actions::buffer_search::{
54    Deploy, DeployReplace, Dismiss, FocusEditor, UseSelectionForFind,
55};
56
57pub enum Event {
58    UpdateLocation,
59    Dismissed,
60}
61
62pub fn init(cx: &mut App) {
63    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
64        .detach();
65}
66
67pub struct BufferSearchBar {
68    query_editor: Entity<Editor>,
69    query_editor_focused: bool,
70    replacement_editor: Entity<Editor>,
71    replacement_editor_focused: bool,
72    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
73    active_match_index: Option<usize>,
74    #[cfg(target_os = "macos")]
75    active_searchable_item_subscriptions: Option<[Subscription; 2]>,
76    #[cfg(not(target_os = "macos"))]
77    active_searchable_item_subscriptions: Option<Subscription>,
78    #[cfg(target_os = "macos")]
79    pending_external_query: Option<(String, SearchOptions)>,
80    active_search: Option<Arc<SearchQuery>>,
81    searchable_items_with_matches:
82        HashMap<Box<dyn WeakSearchableItemHandle>, (AnyVec<dyn Send>, SearchToken)>,
83    pending_search: Option<Task<()>>,
84    search_options: SearchOptions,
85    default_options: SearchOptions,
86    configured_options: SearchOptions,
87    query_error: Option<String>,
88    dismissed: bool,
89    search_history: SearchHistory,
90    search_history_cursor: SearchHistoryCursor,
91    replace_enabled: bool,
92    selection_search_enabled: Option<FilteredSearchRange>,
93    scroll_handle: ScrollHandle,
94    regex_language: Option<Arc<Language>>,
95    splittable_editor: Option<WeakEntity<SplittableEditor>>,
96    _splittable_editor_subscription: Option<Subscription>,
97}
98
99impl EventEmitter<Event> for BufferSearchBar {}
100impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
101impl Render for BufferSearchBar {
102    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
103        let focus_handle = self.focus_handle(cx);
104        let has_splittable_editor = self.splittable_editor.is_some();
105        let split_buttons = self
106            .splittable_editor
107            .as_ref()
108            .and_then(|weak| weak.upgrade())
109            .map(DiffStyleControls::new);
110
111        let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
112            let query_editor_focus = self.query_editor.focus_handle(cx);
113
114            let is_collapsed = self
115                .active_searchable_item
116                .as_ref()
117                .and_then(|item| item.act_as_type(TypeId::of::<Editor>(), cx))
118                .and_then(|item| item.downcast::<Editor>().ok())
119                .map(|editor: Entity<Editor>| editor.read(cx).has_any_buffer_folded(cx))
120                .unwrap_or_default();
121            let (icon, tooltip_label) = if is_collapsed {
122                (IconName::ChevronUpDown, "Expand All Files")
123            } else {
124                (IconName::ChevronDownUp, "Collapse All Files")
125            };
126
127            let collapse_expand_icon_button = |id| {
128                IconButton::new(id, icon)
129                    .icon_size(IconSize::Small)
130                    .tooltip(move |_, cx| {
131                        Tooltip::for_action_in(
132                            tooltip_label,
133                            &ToggleFoldAll,
134                            &query_editor_focus,
135                            cx,
136                        )
137                    })
138                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
139                        this.toggle_fold_all(&ToggleFoldAll, window, cx);
140                    }))
141            };
142
143            if self.dismissed {
144                return h_flex()
145                    .pl_0p5()
146                    .gap_1()
147                    .child(collapse_expand_icon_button(
148                        "multibuffer-collapse-expand-empty",
149                    ))
150                    .when(has_splittable_editor, |this| this.children(split_buttons))
151                    .into_any_element();
152            }
153
154            Some(
155                h_flex()
156                    .gap_1()
157                    .child(collapse_expand_icon_button("multibuffer-collapse-expand"))
158                    .children(split_buttons)
159                    .into_any_element(),
160            )
161        } else {
162            None
163        };
164
165        let narrow_mode =
166            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
167
168        let workspace::searchable::SearchOptions {
169            case,
170            word,
171            regex,
172            replacement,
173            selection,
174            select_all,
175            find_in_results,
176        } = self.supported_options(cx);
177
178        self.query_editor.update(cx, |query_editor, cx| {
179            if query_editor.placeholder_text(cx).is_none() {
180                query_editor.set_placeholder_text("Search…", window, cx);
181            }
182        });
183
184        self.replacement_editor.update(cx, |editor, cx| {
185            editor.set_placeholder_text("Replace with…", window, cx);
186        });
187
188        let mut color_override = None;
189        let match_text = self
190            .active_searchable_item
191            .as_ref()
192            .and_then(|searchable_item| {
193                if self.query(cx).is_empty() {
194                    return None;
195                }
196                let matches_count = self
197                    .searchable_items_with_matches
198                    .get(&searchable_item.downgrade())
199                    .map(|(matches, _)| matches.len())
200                    .unwrap_or(0);
201                if let Some(match_ix) = self.active_match_index {
202                    Some(format!("{}/{}", match_ix + 1, matches_count))
203                } else {
204                    color_override = Some(Color::Error); // No matches found
205                    None
206                }
207            })
208            .unwrap_or_else(|| "0/0".to_string());
209        let should_show_replace_input = self.replace_enabled && replacement;
210        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
211
212        let theme_colors = cx.theme().colors();
213        let query_border = if self.query_error.is_some() {
214            Color::Error.color(cx)
215        } else {
216            theme_colors.border
217        };
218        let replacement_border = theme_colors.border;
219
220        let container_width = window.viewport_size().width;
221        let input_width = SearchInputWidth::calc_width(container_width);
222
223        let input_base_styles =
224            |border_color| input_base_styles(border_color, |div| div.w(input_width));
225
226        let input_style = if find_in_results {
227            filter_search_results_input(query_border, |div| div.w(input_width), cx)
228        } else {
229            input_base_styles(query_border)
230        };
231
232        let query_column = input_style
233            .child(div().flex_1().min_w_0().py_1().child(render_text_input(
234                &self.query_editor,
235                color_override,
236                cx,
237            )))
238            .child(
239                h_flex()
240                    .flex_none()
241                    .gap_1()
242                    .when(case, |div| {
243                        div.child(SearchOption::CaseSensitive.as_button(
244                            self.search_options,
245                            SearchSource::Buffer,
246                            focus_handle.clone(),
247                        ))
248                    })
249                    .when(word, |div| {
250                        div.child(SearchOption::WholeWord.as_button(
251                            self.search_options,
252                            SearchSource::Buffer,
253                            focus_handle.clone(),
254                        ))
255                    })
256                    .when(regex, |div| {
257                        div.child(SearchOption::Regex.as_button(
258                            self.search_options,
259                            SearchSource::Buffer,
260                            focus_handle.clone(),
261                        ))
262                    }),
263            );
264
265        let mode_column = h_flex()
266            .gap_1()
267            .min_w_64()
268            .when(replacement, |this| {
269                this.child(render_action_button(
270                    "buffer-search-bar-toggle",
271                    IconName::Replace,
272                    self.replace_enabled.then_some(ActionButtonState::Toggled),
273                    "Toggle Replace",
274                    &ToggleReplace,
275                    focus_handle.clone(),
276                ))
277            })
278            .when(selection, |this| {
279                this.child(
280                    IconButton::new(
281                        "buffer-search-bar-toggle-search-selection-button",
282                        IconName::Quote,
283                    )
284                    .style(ButtonStyle::Subtle)
285                    .shape(IconButtonShape::Square)
286                    .when(self.selection_search_enabled.is_some(), |button| {
287                        button.style(ButtonStyle::Filled)
288                    })
289                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
290                        this.toggle_selection(&ToggleSelection, window, cx);
291                    }))
292                    .toggle_state(self.selection_search_enabled.is_some())
293                    .tooltip({
294                        let focus_handle = focus_handle.clone();
295                        move |_window, cx| {
296                            Tooltip::for_action_in(
297                                "Toggle Search Selection",
298                                &ToggleSelection,
299                                &focus_handle,
300                                cx,
301                            )
302                        }
303                    }),
304                )
305            })
306            .when(!find_in_results, |el| {
307                let query_focus = self.query_editor.focus_handle(cx);
308                let matches_column = h_flex()
309                    .pl_2()
310                    .ml_2()
311                    .border_l_1()
312                    .border_color(theme_colors.border_variant)
313                    .child(render_action_button(
314                        "buffer-search-nav-button",
315                        ui::IconName::ChevronLeft,
316                        self.active_match_index
317                            .is_none()
318                            .then_some(ActionButtonState::Disabled),
319                        "Select Previous Match",
320                        &SelectPreviousMatch,
321                        query_focus.clone(),
322                    ))
323                    .child(render_action_button(
324                        "buffer-search-nav-button",
325                        ui::IconName::ChevronRight,
326                        self.active_match_index
327                            .is_none()
328                            .then_some(ActionButtonState::Disabled),
329                        "Select Next Match",
330                        &SelectNextMatch,
331                        query_focus.clone(),
332                    ))
333                    .when(!narrow_mode, |this| {
334                        this.child(div().ml_2().min_w(rems_from_px(40.)).child(
335                            Label::new(match_text).size(LabelSize::Small).color(
336                                if self.active_match_index.is_some() {
337                                    Color::Default
338                                } else {
339                                    Color::Disabled
340                                },
341                            ),
342                        ))
343                    });
344
345                el.when(select_all, |el| {
346                    el.child(render_action_button(
347                        "buffer-search-nav-button",
348                        IconName::SelectAll,
349                        Default::default(),
350                        "Select All Matches",
351                        &SelectAllMatches,
352                        query_focus.clone(),
353                    ))
354                })
355                .child(matches_column)
356            })
357            .when(find_in_results, |el| {
358                el.child(render_action_button(
359                    "buffer-search",
360                    IconName::Close,
361                    Default::default(),
362                    "Close Search Bar",
363                    &Dismiss,
364                    focus_handle.clone(),
365                ))
366            });
367
368        let has_collapse_button = collapse_expand_button.is_some();
369
370        let search_line = h_flex()
371            .w_full()
372            .gap_2()
373            .when(find_in_results, |el| el.child(alignment_element()))
374            .when(!find_in_results && has_collapse_button, |el| {
375                el.pl_0p5().child(collapse_expand_button.expect("button"))
376            })
377            .child(query_column)
378            .child(mode_column);
379
380        let replace_line = should_show_replace_input.then(|| {
381            let replace_column = input_base_styles(replacement_border).child(
382                div()
383                    .flex_1()
384                    .py_1()
385                    .child(render_text_input(&self.replacement_editor, None, cx)),
386            );
387            let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
388
389            let replace_actions = h_flex()
390                .min_w_64()
391                .gap_1()
392                .child(render_action_button(
393                    "buffer-search-replace-button",
394                    IconName::ReplaceNext,
395                    Default::default(),
396                    "Replace Next Match",
397                    &ReplaceNext,
398                    focus_handle.clone(),
399                ))
400                .child(render_action_button(
401                    "buffer-search-replace-button",
402                    IconName::ReplaceAll,
403                    Default::default(),
404                    "Replace All Matches",
405                    &ReplaceAll,
406                    focus_handle,
407                ));
408
409            h_flex()
410                .w_full()
411                .gap_2()
412                .when(has_collapse_button, |this| this.child(alignment_element()))
413                .child(replace_column)
414                .child(replace_actions)
415        });
416
417        let mut key_context = KeyContext::new_with_defaults();
418        key_context.add("BufferSearchBar");
419        if in_replace {
420            key_context.add("in_replace");
421        }
422
423        let query_error_line = self.query_error.as_ref().map(|error| {
424            Label::new(error)
425                .size(LabelSize::Small)
426                .color(Color::Error)
427                .mt_neg_1()
428                .ml_2()
429        });
430
431        let search_line =
432            h_flex()
433                .relative()
434                .child(search_line)
435                .when(!narrow_mode && !find_in_results, |this| {
436                    this.child(
437                        h_flex()
438                            .absolute()
439                            .right_0()
440                            .when(has_collapse_button, |this| {
441                                this.pr_2()
442                                    .border_r_1()
443                                    .border_color(cx.theme().colors().border_variant)
444                            })
445                            .child(render_action_button(
446                                "buffer-search",
447                                IconName::Close,
448                                Default::default(),
449                                "Close Search Bar",
450                                &Dismiss,
451                                focus_handle.clone(),
452                            )),
453                    )
454                });
455
456        v_flex()
457            .id("buffer_search")
458            .gap_2()
459            .w_full()
460            .track_scroll(&self.scroll_handle)
461            .key_context(key_context)
462            .capture_action(cx.listener(Self::tab))
463            .capture_action(cx.listener(Self::backtab))
464            .capture_action(cx.listener(Self::toggle_fold_all))
465            .capture_action(cx.listener(Self::toggle_soft_wrap))
466            .on_action(cx.listener(Self::previous_history_query))
467            .on_action(cx.listener(Self::next_history_query))
468            .on_action(cx.listener(Self::dismiss))
469            .on_action(cx.listener(Self::select_next_match))
470            .on_action(cx.listener(Self::select_prev_match))
471            .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
472                if let Some(active_searchable_item) = &mut this.active_searchable_item {
473                    active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
474                }
475            }))
476            .on_action(cx.listener(|this, _: &CopyPath, window, cx| {
477                if let Some(active_searchable_item) = &mut this.active_searchable_item {
478                    active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
479                }
480            }))
481            .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
482                if let Some(active_searchable_item) = &mut this.active_searchable_item {
483                    active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
484                }
485            }))
486            .when(replacement, |this| {
487                this.on_action(cx.listener(Self::toggle_replace))
488                    .on_action(cx.listener(Self::replace_next))
489                    .on_action(cx.listener(Self::replace_all))
490            })
491            .when(case, |this| {
492                this.on_action(cx.listener(Self::toggle_case_sensitive))
493            })
494            .when(word, |this| {
495                this.on_action(cx.listener(Self::toggle_whole_word))
496            })
497            .when(regex, |this| {
498                this.on_action(cx.listener(Self::toggle_regex))
499            })
500            .when(selection, |this| {
501                this.on_action(cx.listener(Self::toggle_selection))
502            })
503            .child(search_line)
504            .children(query_error_line)
505            .children(replace_line)
506            .into_any_element()
507    }
508}
509
510impl Focusable for BufferSearchBar {
511    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
512        self.query_editor.focus_handle(cx)
513    }
514}
515
516impl ToolbarItemView for BufferSearchBar {
517    fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
518        if !self.dismissed {
519            context.add("buffer_search_deployed");
520        }
521    }
522
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_subscriptions.take();
531        self.active_searchable_item.take();
532        self.splittable_editor = None;
533        self._splittable_editor_subscription = None;
534
535        self.pending_search.take();
536
537        if let Some(splittable_editor) = item
538            .and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
539            .and_then(|entity| entity.downcast::<SplittableEditor>().ok())
540        {
541            self._splittable_editor_subscription =
542                Some(cx.observe(&splittable_editor, |_, _, cx| {
543                    cx.notify();
544                }));
545            self.splittable_editor = Some(splittable_editor.downgrade());
546        }
547
548        if let Some(searchable_item_handle) =
549            item.and_then(|item| item.to_searchable_item_handle(cx))
550        {
551            let this = cx.entity().downgrade();
552
553            let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
554                window,
555                cx,
556                Box::new(move |search_event, window, cx| {
557                    if let Some(this) = this.upgrade() {
558                        this.update(cx, |this, cx| {
559                            this.on_active_searchable_item_event(search_event, window, cx)
560                        });
561                    }
562                }),
563            );
564
565            #[cfg(target_os = "macos")]
566            {
567                let item_focus_handle = searchable_item_handle.item_focus_handle(cx);
568
569                self.active_searchable_item_subscriptions = Some([
570                    search_event_subscription,
571                    cx.on_focus(&item_focus_handle, window, |this, window, cx| {
572                        if this.query_editor_focused || this.replacement_editor_focused {
573                            // no need to read pasteboard since focus came from toolbar
574                            return;
575                        }
576
577                        cx.defer_in(window, |this, window, cx| {
578                            let Some(item) = cx.read_from_find_pasteboard() else {
579                                return;
580                            };
581                            let Some(text) = item.text() else {
582                                return;
583                            };
584
585                            if this.query(cx) == text {
586                                return;
587                            }
588
589                            let search_options = item
590                                .metadata()
591                                .and_then(|m| m.parse().ok())
592                                .and_then(SearchOptions::from_bits)
593                                .unwrap_or(this.search_options);
594
595                            if this.dismissed {
596                                this.pending_external_query = Some((text, search_options));
597                            } else {
598                                drop(this.search(&text, Some(search_options), true, window, cx));
599                            }
600                        });
601                    }),
602                ]);
603            }
604            #[cfg(not(target_os = "macos"))]
605            {
606                self.active_searchable_item_subscriptions = Some(search_event_subscription);
607            }
608
609            let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
610            self.active_searchable_item = Some(searchable_item_handle);
611            drop(self.update_matches(true, false, window, cx));
612            if self.needs_expand_collapse_option(cx) && self.is_dismissed() {
613                return ToolbarItemLocation::PrimaryLeft;
614            } else if !self.is_dismissed() {
615                if is_project_search {
616                    self.dismiss(&Default::default(), window, cx);
617                } else {
618                    return self.deployed_toolbar_location(cx);
619                }
620            }
621        }
622        ToolbarItemLocation::Hidden
623    }
624}
625
626impl BufferSearchBar {
627    pub fn query_editor_focused(&self) -> bool {
628        self.query_editor_focused
629    }
630
631    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
632        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
633            this.query_editor.focus_handle(cx).focus(window, cx);
634            this.select_query(window, cx);
635        }));
636        registrar.register_handler(ForDeployed(
637            |this, action: &ToggleCaseSensitive, window, cx| {
638                if this.supported_options(cx).case {
639                    this.toggle_case_sensitive(action, window, cx);
640                }
641            },
642        ));
643        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
644            if this.supported_options(cx).word {
645                this.toggle_whole_word(action, window, cx);
646            }
647        }));
648        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
649            if this.supported_options(cx).regex {
650                this.toggle_regex(action, window, cx);
651            }
652        }));
653        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
654            if this.supported_options(cx).selection {
655                this.toggle_selection(action, window, cx);
656            } else {
657                cx.propagate();
658            }
659        }));
660        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
661            if this.supported_options(cx).replacement {
662                this.toggle_replace(action, window, cx);
663            } else {
664                cx.propagate();
665            }
666        }));
667        registrar.register_handler(WithResultsOrExternalQuery(
668            |this, action: &SelectNextMatch, window, cx| {
669                if this.supported_options(cx).find_in_results {
670                    cx.propagate();
671                } else {
672                    this.select_next_match(action, window, cx);
673                }
674            },
675        ));
676        registrar.register_handler(WithResultsOrExternalQuery(
677            |this, action: &SelectPreviousMatch, window, cx| {
678                if this.supported_options(cx).find_in_results {
679                    cx.propagate();
680                } else {
681                    this.select_prev_match(action, window, cx);
682                }
683            },
684        ));
685        registrar.register_handler(WithResultsOrExternalQuery(
686            |this, action: &SelectAllMatches, window, cx| {
687                if this.supported_options(cx).find_in_results {
688                    cx.propagate();
689                } else {
690                    this.select_all_matches(action, window, cx);
691                }
692            },
693        ));
694        registrar.register_handler(ForDeployed(
695            |this, _: &editor::actions::Cancel, window, cx| {
696                this.dismiss(&Dismiss, window, cx);
697            },
698        ));
699        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
700            this.dismiss(&Dismiss, window, cx);
701        }));
702
703        // register deploy buffer search for both search bar states, since we want to focus into the search bar
704        // when the deploy action is triggered in the buffer.
705        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
706            this.deploy(deploy, None, window, cx);
707        }));
708        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
709            this.deploy(deploy, None, window, cx);
710        }));
711        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
712            if this.supported_options(cx).find_in_results {
713                cx.propagate();
714            } else {
715                this.deploy(&Deploy::replace(), None, window, cx);
716            }
717        }));
718        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
719            if this.supported_options(cx).find_in_results {
720                cx.propagate();
721            } else {
722                this.deploy(&Deploy::replace(), None, window, cx);
723            }
724        }));
725        registrar.register_handler(ForDeployed(
726            |this, action: &UseSelectionForFind, window, cx| {
727                this.use_selection_for_find(action, window, cx);
728            },
729        ));
730        registrar.register_handler(ForDismissed(
731            |this, action: &UseSelectionForFind, window, cx| {
732                this.use_selection_for_find(action, window, cx);
733            },
734        ));
735    }
736
737    pub fn new(
738        languages: Option<Arc<LanguageRegistry>>,
739        window: &mut Window,
740        cx: &mut Context<Self>,
741    ) -> Self {
742        let query_editor = cx.new(|cx| {
743            let mut editor = Editor::auto_height(1, 4, window, cx);
744            editor.set_use_autoclose(false);
745            editor.set_use_selection_highlight(false);
746            editor
747        });
748        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
749            .detach();
750        let replacement_editor = cx.new(|cx| Editor::auto_height(1, 4, window, cx));
751        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
752            .detach();
753
754        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
755        if let Some(languages) = languages {
756            let query_buffer = query_editor
757                .read(cx)
758                .buffer()
759                .read(cx)
760                .as_singleton()
761                .expect("query editor should be backed by a singleton buffer");
762
763            query_buffer
764                .read(cx)
765                .set_language_registry(languages.clone());
766
767            cx.spawn(async move |buffer_search_bar, cx| {
768                use anyhow::Context as _;
769
770                let regex_language = languages
771                    .language_for_name("regex")
772                    .await
773                    .context("loading regex language")?;
774
775                buffer_search_bar
776                    .update(cx, |buffer_search_bar, cx| {
777                        buffer_search_bar.regex_language = Some(regex_language);
778                        buffer_search_bar.adjust_query_regex_language(cx);
779                    })
780                    .ok();
781                anyhow::Ok(())
782            })
783            .detach_and_log_err(cx);
784        }
785
786        Self {
787            query_editor,
788            query_editor_focused: false,
789            replacement_editor,
790            replacement_editor_focused: false,
791            active_searchable_item: None,
792            active_searchable_item_subscriptions: None,
793            #[cfg(target_os = "macos")]
794            pending_external_query: None,
795            active_match_index: None,
796            searchable_items_with_matches: Default::default(),
797            default_options: search_options,
798            configured_options: search_options,
799            search_options,
800            pending_search: None,
801            query_error: None,
802            dismissed: true,
803            search_history: SearchHistory::new(
804                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
805                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
806            ),
807            search_history_cursor: Default::default(),
808            active_search: None,
809            replace_enabled: false,
810            selection_search_enabled: None,
811            scroll_handle: ScrollHandle::new(),
812            regex_language: None,
813            splittable_editor: None,
814            _splittable_editor_subscription: None,
815        }
816    }
817
818    pub fn is_dismissed(&self) -> bool {
819        self.dismissed
820    }
821
822    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
823        self.dismissed = true;
824        cx.emit(Event::Dismissed);
825        self.query_error = None;
826        self.sync_select_next_case_sensitivity(cx);
827
828        for searchable_item in self.searchable_items_with_matches.keys() {
829            if let Some(searchable_item) =
830                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
831            {
832                searchable_item.clear_matches(window, cx);
833            }
834        }
835
836        let needs_collapse_expand = self.needs_expand_collapse_option(cx);
837
838        if let Some(active_editor) = self.active_searchable_item.as_mut() {
839            self.selection_search_enabled = None;
840            self.replace_enabled = false;
841            active_editor.search_bar_visibility_changed(false, window, cx);
842            active_editor.toggle_filtered_search_ranges(None, window, cx);
843            let handle = active_editor.item_focus_handle(cx);
844            self.focus(&handle, window, cx);
845        }
846
847        if needs_collapse_expand {
848            cx.emit(Event::UpdateLocation);
849            cx.emit(ToolbarItemEvent::ChangeLocation(
850                ToolbarItemLocation::PrimaryLeft,
851            ));
852            cx.notify();
853            return;
854        }
855        cx.emit(Event::UpdateLocation);
856        cx.emit(ToolbarItemEvent::ChangeLocation(
857            ToolbarItemLocation::Hidden,
858        ));
859        cx.notify();
860    }
861
862    pub fn deploy(
863        &mut self,
864        deploy: &Deploy,
865        seed_query_override: Option<SeedQuerySetting>,
866        window: &mut Window,
867        cx: &mut Context<Self>,
868    ) -> bool {
869        let filtered_search_range = if deploy.selection_search_enabled {
870            Some(FilteredSearchRange::Default)
871        } else {
872            None
873        };
874        if self.show(window, cx) {
875            if let Some(active_item) = self.active_searchable_item.as_mut() {
876                active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
877            }
878            self.search_suggested(seed_query_override, window, cx);
879            self.smartcase(window, cx);
880            self.sync_select_next_case_sensitivity(cx);
881            self.replace_enabled |= deploy.replace_enabled;
882            self.selection_search_enabled =
883                self.selection_search_enabled
884                    .or(if deploy.selection_search_enabled {
885                        Some(FilteredSearchRange::Default)
886                    } else {
887                        None
888                    });
889            if deploy.focus {
890                let mut handle = self.query_editor.focus_handle(cx);
891                let mut select_query = true;
892
893                let has_seed_text = self
894                    .query_suggestion(seed_query_override, window, cx)
895                    .is_some();
896                if deploy.replace_enabled && has_seed_text {
897                    handle = self.replacement_editor.focus_handle(cx);
898                    select_query = false;
899                };
900
901                if select_query {
902                    self.select_query(window, cx);
903                }
904
905                window.focus(&handle, cx);
906            }
907            return true;
908        }
909
910        cx.propagate();
911        false
912    }
913
914    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
915        if self.is_dismissed() {
916            self.deploy(action, None, window, cx);
917        } else {
918            self.dismiss(&Dismiss, window, cx);
919        }
920    }
921
922    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
923        let Some(handle) = self.active_searchable_item.as_ref() else {
924            return false;
925        };
926
927        let configured_options =
928            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
929        let settings_changed = configured_options != self.configured_options;
930
931        if self.dismissed && settings_changed {
932            // Only update configuration options when search bar is dismissed,
933            // so we don't miss updates even after calling show twice
934            self.configured_options = configured_options;
935            self.search_options = configured_options;
936            self.default_options = configured_options;
937        }
938
939        // This isn't a normal setting; it's only applicable to vim search.
940        self.search_options.remove(SearchOptions::BACKWARDS);
941
942        self.dismissed = false;
943        self.adjust_query_regex_language(cx);
944        handle.search_bar_visibility_changed(true, window, cx);
945        cx.notify();
946        cx.emit(Event::UpdateLocation);
947        cx.emit(ToolbarItemEvent::ChangeLocation(
948            self.deployed_toolbar_location(cx),
949        ));
950        true
951    }
952
953    fn deployed_toolbar_location(&self, cx: &App) -> ToolbarItemLocation {
954        if self.needs_expand_collapse_option(cx) && !self.uses_hidden_splittable_editor(cx) {
955            ToolbarItemLocation::PrimaryLeft
956        } else {
957            ToolbarItemLocation::Secondary
958        }
959    }
960
961    fn uses_hidden_splittable_editor(&self, cx: &App) -> bool {
962        self.splittable_editor.is_none()
963            && self.active_searchable_item.as_ref().is_some_and(|item| {
964                item.act_as_type(TypeId::of::<SplittableEditor>(), cx)
965                    .is_some()
966            })
967    }
968
969    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
970        self.active_searchable_item
971            .as_ref()
972            .map(|item| item.supported_options(cx))
973            .unwrap_or_default()
974    }
975
976    // We provide an expand/collapse button if we are in a multibuffer
977    // and not doing a project search.
978    fn needs_expand_collapse_option(&self, cx: &App) -> bool {
979        self.active_searchable_item.as_ref().is_some_and(|item| {
980            item.buffer_kind(cx) == ItemBufferKind::Multibuffer
981                && !item.supported_options(cx).find_in_results
982        })
983    }
984
985    fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
986        self.toggle_fold_all_in_item(window, cx);
987    }
988
989    // The query editor is an editor itself and would otherwise swallow this
990    // action without any visible effect, so relay it to the searched editor
991    // while keeping the focus in the search bar.
992    fn toggle_soft_wrap(
993        &mut self,
994        action: &ToggleSoftWrap,
995        window: &mut Window,
996        cx: &mut Context<Self>,
997    ) {
998        let Some(item) = &self.active_searchable_item else {
999            return;
1000        };
1001        // A split diff editor picks which side to toggle and keeps both sides
1002        // in sync, while its `act_as_type(Editor)` only exposes the RHS
1003        // editor, so relay to the split editor itself when it is split.
1004        if let Some(split_editor) = item.act_as_type(TypeId::of::<SplittableEditor>(), cx) {
1005            let split_editor = split_editor
1006                .downcast::<SplittableEditor>()
1007                .expect("Is a splittable editor");
1008            if split_editor.read(cx).is_split() {
1009                split_editor.update(cx, |split_editor, cx| {
1010                    split_editor.toggle_soft_wrap(action, window, cx)
1011                });
1012                return;
1013            }
1014        }
1015        if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
1016            let editor = item.downcast::<Editor>().expect("Is an editor");
1017            editor.update(cx, |editor, cx| editor.toggle_soft_wrap(action, window, cx));
1018        }
1019    }
1020
1021    fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
1022        if let Some(item) = &self.active_searchable_item {
1023            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
1024                let editor = item.downcast::<Editor>().expect("Is an editor");
1025                editor.update(cx, |editor, cx| {
1026                    let is_collapsed = editor.has_any_buffer_folded(cx);
1027                    if is_collapsed {
1028                        editor.unfold_all(&UnfoldAll, window, cx);
1029                    } else {
1030                        editor.fold_all(&FoldAll, window, cx);
1031                    }
1032                })
1033            }
1034        }
1035    }
1036
1037    pub fn search_suggested(
1038        &mut self,
1039        seed_query_override: Option<SeedQuerySetting>,
1040        window: &mut Window,
1041        cx: &mut Context<Self>,
1042    ) {
1043        let search = self
1044            .query_suggestion(seed_query_override, window, cx)
1045            .map(|suggestion| {
1046                let suggestion = if self.default_options.contains(SearchOptions::REGEX) {
1047                    regex::escape(&suggestion)
1048                } else {
1049                    suggestion
1050                };
1051                self.search(&suggestion, Some(self.default_options), true, window, cx)
1052            });
1053
1054        #[cfg(target_os = "macos")]
1055        let search = search.or_else(|| {
1056            self.pending_external_query
1057                .take()
1058                .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
1059        });
1060
1061        if let Some(search) = search {
1062            cx.spawn_in(window, async move |this, cx| {
1063                if search.await.is_ok() {
1064                    this.update_in(cx, |this, window, cx| {
1065                        if !this.dismissed {
1066                            this.activate_current_match(window, cx)
1067                        }
1068                    })
1069                } else {
1070                    Ok(())
1071                }
1072            })
1073            .detach_and_log_err(cx);
1074        }
1075    }
1076
1077    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1078        if let Some(match_ix) = self.active_match_index
1079            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
1080            && let Some((matches, token)) = self
1081                .searchable_items_with_matches
1082                .get(&active_searchable_item.downgrade())
1083        {
1084            active_searchable_item.activate_match(match_ix, matches, *token, window, cx)
1085        }
1086    }
1087
1088    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1089        self.query_editor.update(cx, |query_editor, cx| {
1090            query_editor.select_all(&Default::default(), window, cx);
1091        });
1092    }
1093
1094    pub fn query(&self, cx: &App) -> String {
1095        self.query_editor.read(cx).text(cx)
1096    }
1097
1098    pub fn replacement(&self, cx: &mut App) -> String {
1099        self.replacement_editor.read(cx).text(cx)
1100    }
1101
1102    pub fn query_suggestion(
1103        &mut self,
1104        seed_query_override: Option<SeedQuerySetting>,
1105        window: &mut Window,
1106        cx: &mut Context<Self>,
1107    ) -> Option<String> {
1108        self.active_searchable_item
1109            .as_ref()
1110            .map(|searchable_item| {
1111                searchable_item.query_suggestion(seed_query_override, window, cx)
1112            })
1113            .filter(|suggestion| !suggestion.is_empty())
1114    }
1115
1116    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
1117        if replacement.is_none() {
1118            self.replace_enabled = false;
1119            return;
1120        }
1121        self.replace_enabled = true;
1122        self.replacement_editor
1123            .update(cx, |replacement_editor, cx| {
1124                replacement_editor
1125                    .buffer()
1126                    .update(cx, |replacement_buffer, cx| {
1127                        let len = replacement_buffer.len(cx);
1128                        replacement_buffer.edit(
1129                            [(MultiBufferOffset(0)..len, replacement.unwrap())],
1130                            None,
1131                            cx,
1132                        );
1133                    });
1134            });
1135    }
1136
1137    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1138        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
1139        cx.notify();
1140    }
1141
1142    pub fn search(
1143        &mut self,
1144        query: &str,
1145        options: Option<SearchOptions>,
1146        add_to_history: bool,
1147        window: &mut Window,
1148        cx: &mut Context<Self>,
1149    ) -> oneshot::Receiver<()> {
1150        let options = options.unwrap_or(self.default_options);
1151        let updated = query != self.query(cx) || self.search_options != options;
1152        if updated {
1153            self.query_editor.update(cx, |query_editor, cx| {
1154                query_editor.buffer().update(cx, |query_buffer, cx| {
1155                    let len = query_buffer.len(cx);
1156                    query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
1157                });
1158                query_editor.request_autoscroll(Autoscroll::fit(), cx);
1159            });
1160            self.set_search_options(options, cx);
1161            self.clear_matches(window, cx);
1162            #[cfg(target_os = "macos")]
1163            self.update_find_pasteboard(cx);
1164            cx.notify();
1165        }
1166        self.update_matches(!updated, add_to_history, window, cx)
1167    }
1168
1169    #[cfg(target_os = "macos")]
1170    pub fn update_find_pasteboard(&mut self, cx: &mut App) {
1171        cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
1172            self.query(cx),
1173            self.search_options.bits().to_string(),
1174        ));
1175    }
1176
1177    pub fn use_selection_for_find(
1178        &mut self,
1179        _: &UseSelectionForFind,
1180        window: &mut Window,
1181        cx: &mut Context<Self>,
1182    ) {
1183        self.deploy(
1184            &Deploy {
1185                focus: false,
1186                replace_enabled: false,
1187                selection_search_enabled: false,
1188            },
1189            Some(SeedQuerySetting::Always),
1190            window,
1191            cx,
1192        );
1193    }
1194
1195    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
1196        if let Some(active_editor) = self.active_searchable_item.as_ref() {
1197            let handle = active_editor.item_focus_handle(cx);
1198            window.focus(&handle, cx);
1199        }
1200    }
1201
1202    pub fn toggle_search_option(
1203        &mut self,
1204        search_option: SearchOptions,
1205        window: &mut Window,
1206        cx: &mut Context<Self>,
1207    ) {
1208        self.search_options.toggle(search_option);
1209        self.default_options = self.search_options;
1210        drop(self.update_matches(false, false, window, cx));
1211        self.adjust_query_regex_language(cx);
1212        self.sync_select_next_case_sensitivity(cx);
1213        cx.notify();
1214    }
1215
1216    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
1217        self.search_options.contains(search_option)
1218    }
1219
1220    pub fn enable_search_option(
1221        &mut self,
1222        search_option: SearchOptions,
1223        window: &mut Window,
1224        cx: &mut Context<Self>,
1225    ) {
1226        if !self.search_options.contains(search_option) {
1227            self.toggle_search_option(search_option, window, cx)
1228        }
1229    }
1230
1231    pub fn set_search_within_selection(
1232        &mut self,
1233        search_within_selection: Option<FilteredSearchRange>,
1234        window: &mut Window,
1235        cx: &mut Context<Self>,
1236    ) -> Option<oneshot::Receiver<()>> {
1237        let active_item = self.active_searchable_item.as_mut()?;
1238        self.selection_search_enabled = search_within_selection;
1239        active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1240        cx.notify();
1241        Some(self.update_matches(false, false, window, cx))
1242    }
1243
1244    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
1245        self.search_options = search_options;
1246        self.adjust_query_regex_language(cx);
1247        self.sync_select_next_case_sensitivity(cx);
1248        cx.notify();
1249    }
1250
1251    pub fn clear_search_within_ranges(
1252        &mut self,
1253        search_options: SearchOptions,
1254        cx: &mut Context<Self>,
1255    ) {
1256        self.search_options = search_options;
1257        self.adjust_query_regex_language(cx);
1258        cx.notify();
1259    }
1260
1261    fn select_next_match(
1262        &mut self,
1263        _: &SelectNextMatch,
1264        window: &mut Window,
1265        cx: &mut Context<Self>,
1266    ) {
1267        self.select_match(Direction::Next, 1, window, cx);
1268    }
1269
1270    fn select_prev_match(
1271        &mut self,
1272        _: &SelectPreviousMatch,
1273        window: &mut Window,
1274        cx: &mut Context<Self>,
1275    ) {
1276        self.select_match(Direction::Prev, 1, window, cx);
1277    }
1278
1279    pub fn select_all_matches(
1280        &mut self,
1281        _: &SelectAllMatches,
1282        window: &mut Window,
1283        cx: &mut Context<Self>,
1284    ) {
1285        if !self.dismissed
1286            && self.active_match_index.is_some()
1287            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1288            && let Some((matches, token)) = self
1289                .searchable_items_with_matches
1290                .get(&searchable_item.downgrade())
1291        {
1292            searchable_item.select_matches(matches, *token, window, cx);
1293            self.focus_editor(&FocusEditor, window, cx);
1294        }
1295    }
1296
1297    pub fn select_match(
1298        &mut self,
1299        direction: Direction,
1300        count: usize,
1301        window: &mut Window,
1302        cx: &mut Context<Self>,
1303    ) {
1304        #[cfg(target_os = "macos")]
1305        if let Some((query, options)) = self.pending_external_query.take() {
1306            let search_rx = self.search(&query, Some(options), true, window, cx);
1307            cx.spawn_in(window, async move |this, cx| {
1308                if search_rx.await.is_ok() {
1309                    this.update_in(cx, |this, window, cx| {
1310                        this.activate_current_match(window, cx);
1311                    })
1312                    .ok();
1313                }
1314            })
1315            .detach();
1316
1317            return;
1318        }
1319
1320        if let Some(index) = self.active_match_index
1321            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1322            && let Some((matches, token)) = self
1323                .searchable_items_with_matches
1324                .get(&searchable_item.downgrade())
1325                .filter(|(matches, _)| !matches.is_empty())
1326        {
1327            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
1328            if !EditorSettings::get_global(cx).search_wrap
1329                && ((direction == Direction::Next && index + count >= matches.len())
1330                    || (direction == Direction::Prev && index < count))
1331            {
1332                crate::show_no_more_matches(window, cx);
1333                return;
1334            }
1335            let new_match_index = searchable_item
1336                .match_index_for_direction(matches, index, direction, count, *token, window, cx);
1337            self.active_match_index = Some(new_match_index);
1338
1339            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1340            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1341        }
1342    }
1343
1344    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1345        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1346            && let Some((matches, token)) = self
1347                .searchable_items_with_matches
1348                .get(&searchable_item.downgrade())
1349        {
1350            if matches.is_empty() {
1351                return;
1352            }
1353            searchable_item.update_matches(matches, Some(0), *token, window, cx);
1354            searchable_item.activate_match(0, matches, *token, window, cx);
1355        }
1356    }
1357
1358    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1359        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1360            && let Some((matches, token)) = self
1361                .searchable_items_with_matches
1362                .get(&searchable_item.downgrade())
1363        {
1364            if matches.is_empty() {
1365                return;
1366            }
1367            let new_match_index = matches.len() - 1;
1368            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
1369            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
1370        }
1371    }
1372
1373    fn on_query_editor_event(
1374        &mut self,
1375        _editor: &Entity<Editor>,
1376        event: &editor::EditorEvent,
1377        window: &mut Window,
1378        cx: &mut Context<Self>,
1379    ) {
1380        match event {
1381            editor::EditorEvent::Focused => self.query_editor_focused = true,
1382            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1383            editor::EditorEvent::Edited { .. } => {
1384                self.smartcase(window, cx);
1385                self.clear_matches(window, cx);
1386                let search = self.update_matches(false, true, window, cx);
1387
1388                cx.spawn_in(window, async move |this, cx| {
1389                    if search.await.is_ok() {
1390                        this.update_in(cx, |this, window, cx| {
1391                            this.activate_current_match(window, cx);
1392                            #[cfg(target_os = "macos")]
1393                            this.update_find_pasteboard(cx);
1394                        })?;
1395                    }
1396                    anyhow::Ok(())
1397                })
1398                .detach_and_log_err(cx);
1399            }
1400            _ => {}
1401        }
1402    }
1403
1404    fn on_replacement_editor_event(
1405        &mut self,
1406        _: Entity<Editor>,
1407        event: &editor::EditorEvent,
1408        _: &mut Context<Self>,
1409    ) {
1410        match event {
1411            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1412            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1413            _ => {}
1414        }
1415    }
1416
1417    fn on_active_searchable_item_event(
1418        &mut self,
1419        event: &SearchEvent,
1420        window: &mut Window,
1421        cx: &mut Context<Self>,
1422    ) {
1423        match event {
1424            SearchEvent::MatchesInvalidated => {
1425                drop(self.update_matches(false, false, window, cx));
1426            }
1427            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1428        }
1429    }
1430
1431    fn toggle_case_sensitive(
1432        &mut self,
1433        _: &ToggleCaseSensitive,
1434        window: &mut Window,
1435        cx: &mut Context<Self>,
1436    ) {
1437        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1438    }
1439
1440    fn toggle_whole_word(
1441        &mut self,
1442        _: &ToggleWholeWord,
1443        window: &mut Window,
1444        cx: &mut Context<Self>,
1445    ) {
1446        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1447    }
1448
1449    fn toggle_selection(
1450        &mut self,
1451        _: &ToggleSelection,
1452        window: &mut Window,
1453        cx: &mut Context<Self>,
1454    ) {
1455        self.set_search_within_selection(
1456            if let Some(_) = self.selection_search_enabled {
1457                None
1458            } else {
1459                Some(FilteredSearchRange::Default)
1460            },
1461            window,
1462            cx,
1463        );
1464    }
1465
1466    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1467        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1468    }
1469
1470    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1471        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1472            self.active_match_index = None;
1473            self.searchable_items_with_matches
1474                .remove(&active_searchable_item.downgrade());
1475            active_searchable_item.clear_matches(window, cx);
1476        }
1477    }
1478
1479    pub fn has_active_match(&self) -> bool {
1480        self.active_match_index.is_some()
1481    }
1482
1483    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1484        let mut active_item_matches = None;
1485        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1486            if let Some(searchable_item) =
1487                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1488            {
1489                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1490                    active_item_matches = Some((searchable_item.downgrade(), matches));
1491                } else {
1492                    searchable_item.clear_matches(window, cx);
1493                }
1494            }
1495        }
1496
1497        self.searchable_items_with_matches
1498            .extend(active_item_matches);
1499    }
1500
1501    fn update_matches(
1502        &mut self,
1503        reuse_existing_query: bool,
1504        add_to_history: bool,
1505        window: &mut Window,
1506        cx: &mut Context<Self>,
1507    ) -> oneshot::Receiver<()> {
1508        let (done_tx, done_rx) = oneshot::channel();
1509        let query = self.query(cx);
1510        self.pending_search.take();
1511        #[cfg(target_os = "macos")]
1512        self.pending_external_query.take();
1513
1514        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1515            self.query_error = None;
1516            if query.is_empty() {
1517                self.clear_active_searchable_item_matches(window, cx);
1518                let _ = done_tx.send(());
1519                cx.notify();
1520            } else {
1521                let query: Arc<_> = if let Some(search) =
1522                    self.active_search.take().filter(|_| reuse_existing_query)
1523                {
1524                    search
1525                } else {
1526                    // Value doesn't matter, we only construct empty matchers with it
1527
1528                    if self.search_options.contains(SearchOptions::REGEX) {
1529                        match SearchQuery::regex(
1530                            query,
1531                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1532                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1533                            false,
1534                            self.search_options
1535                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1536                            PathMatcher::default(),
1537                            PathMatcher::default(),
1538                            false,
1539                            None,
1540                        ) {
1541                            Ok(query) => query.with_replacement(self.replacement(cx)),
1542                            Err(e) => {
1543                                self.query_error = Some(e.to_string());
1544                                self.clear_active_searchable_item_matches(window, cx);
1545                                cx.notify();
1546                                return done_rx;
1547                            }
1548                        }
1549                    } else {
1550                        match SearchQuery::text(
1551                            query,
1552                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1553                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1554                            false,
1555                            PathMatcher::default(),
1556                            PathMatcher::default(),
1557                            false,
1558                            None,
1559                        ) {
1560                            Ok(query) => query.with_replacement(self.replacement(cx)),
1561                            Err(e) => {
1562                                self.query_error = Some(e.to_string());
1563                                self.clear_active_searchable_item_matches(window, cx);
1564                                cx.notify();
1565                                return done_rx;
1566                            }
1567                        }
1568                    }
1569                    .into()
1570                };
1571
1572                self.active_search = Some(query.clone());
1573                let query_text = query.as_str().to_string();
1574
1575                let matches_with_token =
1576                    active_searchable_item.find_matches_with_token(query, window, cx);
1577
1578                let active_searchable_item = active_searchable_item.downgrade();
1579                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1580                    let (matches, token) = matches_with_token.await;
1581
1582                    this.update_in(cx, |this, window, cx| {
1583                        if let Some(active_searchable_item) =
1584                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1585                        {
1586                            this.searchable_items_with_matches
1587                                .insert(active_searchable_item.downgrade(), (matches, token));
1588
1589                            this.update_match_index(window, cx);
1590
1591                            if add_to_history {
1592                                this.search_history
1593                                    .add(&mut this.search_history_cursor, query_text);
1594                            }
1595                            if !this.dismissed {
1596                                let (matches, token) = this
1597                                    .searchable_items_with_matches
1598                                    .get(&active_searchable_item.downgrade())
1599                                    .unwrap();
1600                                if matches.is_empty() {
1601                                    active_searchable_item.clear_matches(window, cx);
1602                                } else {
1603                                    active_searchable_item.update_matches(
1604                                        matches,
1605                                        this.active_match_index,
1606                                        *token,
1607                                        window,
1608                                        cx,
1609                                    );
1610                                }
1611                            }
1612                            let _ = done_tx.send(());
1613                            cx.notify();
1614                        }
1615                    })
1616                    .log_err();
1617                }));
1618            }
1619        }
1620        done_rx
1621    }
1622
1623    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1624        if self.search_options.contains(SearchOptions::BACKWARDS) {
1625            direction.opposite()
1626        } else {
1627            direction
1628        }
1629    }
1630
1631    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1632        let direction = self.reverse_direction_if_backwards(Direction::Next);
1633        let new_index = self
1634            .active_searchable_item
1635            .as_ref()
1636            .and_then(|searchable_item| {
1637                let (matches, token) = self
1638                    .searchable_items_with_matches
1639                    .get(&searchable_item.downgrade())?;
1640                searchable_item.active_match_index(direction, matches, *token, window, cx)
1641            });
1642        if new_index != self.active_match_index {
1643            self.active_match_index = new_index;
1644            if !self.dismissed {
1645                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1646                    if let Some((matches, token)) = self
1647                        .searchable_items_with_matches
1648                        .get(&searchable_item.downgrade())
1649                    {
1650                        if !matches.is_empty() {
1651                            searchable_item.update_matches(matches, new_index, *token, window, cx);
1652                        }
1653                    }
1654                }
1655            }
1656            cx.notify();
1657        }
1658    }
1659
1660    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1661        self.cycle_field(Direction::Next, window, cx);
1662    }
1663
1664    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1665        self.cycle_field(Direction::Prev, window, cx);
1666    }
1667    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1668        let mut handles = vec![self.query_editor.focus_handle(cx)];
1669        if self.replace_enabled {
1670            handles.push(self.replacement_editor.focus_handle(cx));
1671        }
1672        if let Some(item) = self.active_searchable_item.as_ref() {
1673            handles.push(item.item_focus_handle(cx));
1674        }
1675        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1676            Some(index) => index,
1677            None => return,
1678        };
1679
1680        let new_index = match direction {
1681            Direction::Next => (current_index + 1) % handles.len(),
1682            Direction::Prev if current_index == 0 => handles.len() - 1,
1683            Direction::Prev => (current_index - 1) % handles.len(),
1684        };
1685        let next_focus_handle = &handles[new_index];
1686        self.focus(next_focus_handle, window, cx);
1687        cx.stop_propagation();
1688    }
1689
1690    fn next_history_query(
1691        &mut self,
1692        _: &NextHistoryQuery,
1693        window: &mut Window,
1694        cx: &mut Context<Self>,
1695    ) {
1696        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Next, cx) {
1697            cx.propagate();
1698            return;
1699        }
1700
1701        if let Some(new_query) = self
1702            .search_history
1703            .next(&mut self.search_history_cursor)
1704            .map(str::to_string)
1705        {
1706            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1707        } else if let Some(draft) = self.search_history_cursor.take_draft() {
1708            drop(self.search(&draft, Some(self.search_options), false, window, cx));
1709        }
1710    }
1711
1712    fn previous_history_query(
1713        &mut self,
1714        _: &PreviousHistoryQuery,
1715        window: &mut Window,
1716        cx: &mut Context<Self>,
1717    ) {
1718        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Previous, cx) {
1719            cx.propagate();
1720            return;
1721        }
1722
1723        if self.query(cx).is_empty()
1724            && let Some(new_query) = self
1725                .search_history
1726                .current(&self.search_history_cursor)
1727                .map(str::to_string)
1728        {
1729            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1730            return;
1731        }
1732
1733        let current_query = self.query(cx);
1734        if let Some(new_query) = self
1735            .search_history
1736            .previous(&mut self.search_history_cursor, &current_query)
1737            .map(str::to_string)
1738        {
1739            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
1740        }
1741    }
1742
1743    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
1744        window.invalidate_character_coordinates();
1745        window.focus(handle, cx);
1746    }
1747
1748    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1749        if self.active_searchable_item.is_some() {
1750            self.replace_enabled = !self.replace_enabled;
1751            let handle = if self.replace_enabled {
1752                self.replacement_editor.focus_handle(cx)
1753            } else {
1754                self.query_editor.focus_handle(cx)
1755            };
1756            self.focus(&handle, window, cx);
1757            cx.notify();
1758        }
1759    }
1760
1761    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1762        let mut should_propagate = true;
1763        if !self.dismissed
1764            && self.active_search.is_some()
1765            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1766            && let Some(query) = self.active_search.as_ref()
1767            && let Some((matches, token)) = self
1768                .searchable_items_with_matches
1769                .get(&searchable_item.downgrade())
1770        {
1771            if let Some(active_index) = self.active_match_index {
1772                let query = query
1773                    .as_ref()
1774                    .clone()
1775                    .with_replacement(self.replacement(cx));
1776                searchable_item.replace(matches.at(active_index), &query, *token, window, cx);
1777                self.select_next_match(&SelectNextMatch, window, cx);
1778            }
1779            should_propagate = false;
1780        }
1781        if !should_propagate {
1782            cx.stop_propagation();
1783        }
1784    }
1785
1786    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1787        if !self.dismissed
1788            && self.active_search.is_some()
1789            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1790            && let Some(query) = self.active_search.as_ref()
1791            && let Some((matches, token)) = self
1792                .searchable_items_with_matches
1793                .get(&searchable_item.downgrade())
1794        {
1795            let query = query
1796                .as_ref()
1797                .clone()
1798                .with_replacement(self.replacement(cx));
1799            searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx);
1800        }
1801    }
1802
1803    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1804        self.update_match_index(window, cx);
1805        self.active_match_index.is_some()
1806    }
1807
1808    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1809        EditorSettings::get_global(cx).use_smartcase_search
1810    }
1811
1812    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1813        str.chars().any(|c| c.is_uppercase())
1814    }
1815
1816    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1817        if self.should_use_smartcase_search(cx) {
1818            let query = self.query(cx);
1819            if !query.is_empty() {
1820                let is_case = self.is_contains_uppercase(&query);
1821                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1822                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1823                }
1824            }
1825        }
1826    }
1827
1828    fn adjust_query_regex_language(&self, cx: &mut App) {
1829        let enable = self.search_options.contains(SearchOptions::REGEX);
1830        let query_buffer = self
1831            .query_editor
1832            .read(cx)
1833            .buffer()
1834            .read(cx)
1835            .as_singleton()
1836            .expect("query editor should be backed by a singleton buffer");
1837
1838        if enable {
1839            if let Some(regex_language) = self.regex_language.clone() {
1840                query_buffer.update(cx, |query_buffer, cx| {
1841                    query_buffer.set_language(Some(regex_language), cx);
1842                })
1843            }
1844        } else {
1845            query_buffer.update(cx, |query_buffer, cx| {
1846                query_buffer.set_language(None, cx);
1847            })
1848        }
1849    }
1850
1851    /// Updates the searchable item's case sensitivity option to match the
1852    /// search bar's current case sensitivity setting. This ensures that
1853    /// editor's `select_next`/ `select_previous` operations respect the buffer
1854    /// search bar's search options.
1855    ///
1856    /// Clears the case sensitivity when the search bar is dismissed so that
1857    /// only the editor's settings are respected.
1858    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
1859        let case_sensitive = match self.dismissed {
1860            true => None,
1861            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
1862        };
1863
1864        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1865            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
1866        }
1867    }
1868}
1869
1870#[cfg(test)]
1871mod tests {
1872    use std::{ops::Range, time::Duration};
1873
1874    use super::*;
1875    use editor::{
1876        DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
1877        SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects, SoftWrap,
1878        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
1879    };
1880    use futures::stream::StreamExt as _;
1881    use gpui::{FocusHandle, Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1882    use language::{Buffer, Point};
1883    #[cfg(target_os = "macos")]
1884    use project::Project;
1885    use settings::{SearchSettingsContent, SettingsStore};
1886    use unindent::Unindent as _;
1887    use util_macros::perf;
1888    #[cfg(target_os = "macos")]
1889    use workspace::{AppState, MultiWorkspace, Workspace};
1890    use workspace::{item::Item, searchable::SearchableItem};
1891
1892    fn init_globals(cx: &mut TestAppContext) {
1893        cx.update(|cx| {
1894            let store = settings::SettingsStore::test(cx);
1895            cx.set_global(store);
1896            editor::init(cx);
1897
1898            theme_settings::init(theme::LoadThemes::JustBase, cx);
1899            crate::init(cx);
1900        });
1901    }
1902
1903    fn init_multibuffer_test(
1904        cx: &mut TestAppContext,
1905    ) -> (
1906        Entity<Editor>,
1907        Entity<BufferSearchBar>,
1908        &mut VisualTestContext,
1909    ) {
1910        init_globals(cx);
1911
1912        let buffer1 = cx.new(|cx| {
1913            Buffer::local(
1914                            r#"
1915                            A regular expression (shortened as regex or regexp;[1] also referred to as
1916                            rational expression[2][3]) is a sequence of characters that specifies a search
1917                            pattern in text. Usually such patterns are used by string-searching algorithms
1918                            for "find" or "find and replace" operations on strings, or for input validation.
1919                            "#
1920                            .unindent(),
1921                            cx,
1922                        )
1923        });
1924
1925        let buffer2 = cx.new(|cx| {
1926            Buffer::local(
1927                r#"
1928                            Some Additional text with the term regular expression in it.
1929                            There two lines.
1930                            "#
1931                .unindent(),
1932                cx,
1933            )
1934        });
1935
1936        let multibuffer = cx.new(|cx| {
1937            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);
1938
1939            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
1940            buffer.set_excerpts_for_path(
1941                PathKey::sorted(0),
1942                buffer1,
1943                [Point::new(0, 0)..Point::new(3, 0)],
1944                0,
1945                cx,
1946            );
1947            buffer.set_excerpts_for_path(
1948                PathKey::sorted(1),
1949                buffer2,
1950                [Point::new(0, 0)..Point::new(1, 0)],
1951                0,
1952                cx,
1953            );
1954
1955            buffer
1956        });
1957        let mut editor = None;
1958        let window = cx.add_window(|window, cx| {
1959            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1960                "keymaps/default-macos.json",
1961                cx,
1962            )
1963            .unwrap();
1964            cx.bind_keys(default_key_bindings);
1965            editor =
1966                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));
1967
1968            let mut search_bar = BufferSearchBar::new(None, window, cx);
1969            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1970            search_bar.show(window, cx);
1971            search_bar
1972        });
1973        let search_bar = window.root(cx).unwrap();
1974
1975        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1976
1977        (editor.unwrap(), search_bar, cx)
1978    }
1979
1980    fn init_test(
1981        cx: &mut TestAppContext,
1982    ) -> (
1983        Entity<Editor>,
1984        Entity<BufferSearchBar>,
1985        &mut VisualTestContext,
1986    ) {
1987        init_globals(cx);
1988        let buffer = cx.new(|cx| {
1989            Buffer::local(
1990                r#"
1991                A regular expression (shortened as regex or regexp;[1] also referred to as
1992                rational expression[2][3]) is a sequence of characters that specifies a search
1993                pattern in text. Usually such patterns are used by string-searching algorithms
1994                for "find" or "find and replace" operations on strings, or for input validation.
1995                "#
1996                .unindent(),
1997                cx,
1998            )
1999        });
2000        let mut editor = None;
2001        let window = cx.add_window(|window, cx| {
2002            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
2003                "keymaps/default-macos.json",
2004                cx,
2005            )
2006            .unwrap();
2007            cx.bind_keys(default_key_bindings);
2008            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
2009            let mut search_bar = BufferSearchBar::new(None, window, cx);
2010            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
2011            search_bar.show(window, cx);
2012            search_bar
2013        });
2014        let search_bar = window.root(cx).unwrap();
2015
2016        let cx = VisualTestContext::from_window(*window, cx).into_mut();
2017
2018        (editor.unwrap(), search_bar, cx)
2019    }
2020
2021    #[perf]
2022    #[gpui::test]
2023    async fn test_search_simple(cx: &mut TestAppContext) {
2024        let (editor, search_bar, cx) = init_test(cx);
2025        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
2026            background_highlights
2027                .into_iter()
2028                .map(|(range, _)| range)
2029                .collect::<Vec<_>>()
2030        };
2031        // Search for a string that appears with different casing.
2032        // By default, search is case-insensitive.
2033        search_bar
2034            .update_in(cx, |search_bar, window, cx| {
2035                search_bar.search("us", None, true, window, cx)
2036            })
2037            .await
2038            .unwrap();
2039        editor.update_in(cx, |editor, window, cx| {
2040            assert_eq!(
2041                display_points_of(editor.all_text_background_highlights(window, cx)),
2042                &[
2043                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
2044                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
2045                ]
2046            );
2047        });
2048
2049        // Switch to a case sensitive search.
2050        search_bar.update_in(cx, |search_bar, window, cx| {
2051            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2052        });
2053        let mut editor_notifications = cx.notifications(&editor);
2054        editor_notifications.next().await;
2055        editor.update_in(cx, |editor, window, cx| {
2056            assert_eq!(
2057                display_points_of(editor.all_text_background_highlights(window, cx)),
2058                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2059            );
2060        });
2061
2062        // Search for a string that appears both as a whole word and
2063        // within other words. By default, all results are found.
2064        search_bar
2065            .update_in(cx, |search_bar, window, cx| {
2066                search_bar.search("or", None, true, window, cx)
2067            })
2068            .await
2069            .unwrap();
2070        editor.update_in(cx, |editor, window, cx| {
2071            assert_eq!(
2072                display_points_of(editor.all_text_background_highlights(window, cx)),
2073                &[
2074                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
2075                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2076                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
2077                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
2078                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2079                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2080                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
2081                ]
2082            );
2083        });
2084
2085        // Switch to a whole word search.
2086        search_bar.update_in(cx, |search_bar, window, cx| {
2087            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2088        });
2089        let mut editor_notifications = cx.notifications(&editor);
2090        editor_notifications.next().await;
2091        editor.update_in(cx, |editor, window, cx| {
2092            assert_eq!(
2093                display_points_of(editor.all_text_background_highlights(window, cx)),
2094                &[
2095                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
2096                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
2097                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
2098                ]
2099            );
2100        });
2101
2102        editor.update_in(cx, |editor, window, cx| {
2103            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2104                s.select_display_ranges([
2105                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2106                ])
2107            });
2108        });
2109        search_bar.update_in(cx, |search_bar, window, cx| {
2110            assert_eq!(search_bar.active_match_index, Some(0));
2111            search_bar.select_next_match(&SelectNextMatch, window, cx);
2112            assert_eq!(
2113                editor.update(cx, |editor, cx| editor
2114                    .selections
2115                    .display_ranges(&editor.display_snapshot(cx))),
2116                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2117            );
2118        });
2119        search_bar.read_with(cx, |search_bar, _| {
2120            assert_eq!(search_bar.active_match_index, Some(0));
2121        });
2122
2123        search_bar.update_in(cx, |search_bar, window, cx| {
2124            search_bar.select_next_match(&SelectNextMatch, window, cx);
2125            assert_eq!(
2126                editor.update(cx, |editor, cx| editor
2127                    .selections
2128                    .display_ranges(&editor.display_snapshot(cx))),
2129                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2130            );
2131        });
2132        search_bar.read_with(cx, |search_bar, _| {
2133            assert_eq!(search_bar.active_match_index, Some(1));
2134        });
2135
2136        search_bar.update_in(cx, |search_bar, window, cx| {
2137            search_bar.select_next_match(&SelectNextMatch, window, cx);
2138            assert_eq!(
2139                editor.update(cx, |editor, cx| editor
2140                    .selections
2141                    .display_ranges(&editor.display_snapshot(cx))),
2142                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2143            );
2144        });
2145        search_bar.read_with(cx, |search_bar, _| {
2146            assert_eq!(search_bar.active_match_index, Some(2));
2147        });
2148
2149        search_bar.update_in(cx, |search_bar, window, cx| {
2150            search_bar.select_next_match(&SelectNextMatch, window, cx);
2151            assert_eq!(
2152                editor.update(cx, |editor, cx| editor
2153                    .selections
2154                    .display_ranges(&editor.display_snapshot(cx))),
2155                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2156            );
2157        });
2158        search_bar.read_with(cx, |search_bar, _| {
2159            assert_eq!(search_bar.active_match_index, Some(0));
2160        });
2161
2162        search_bar.update_in(cx, |search_bar, window, cx| {
2163            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2164            assert_eq!(
2165                editor.update(cx, |editor, cx| editor
2166                    .selections
2167                    .display_ranges(&editor.display_snapshot(cx))),
2168                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2169            );
2170        });
2171        search_bar.read_with(cx, |search_bar, _| {
2172            assert_eq!(search_bar.active_match_index, Some(2));
2173        });
2174
2175        search_bar.update_in(cx, |search_bar, window, cx| {
2176            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2177            assert_eq!(
2178                editor.update(cx, |editor, cx| editor
2179                    .selections
2180                    .display_ranges(&editor.display_snapshot(cx))),
2181                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2182            );
2183        });
2184        search_bar.read_with(cx, |search_bar, _| {
2185            assert_eq!(search_bar.active_match_index, Some(1));
2186        });
2187
2188        search_bar.update_in(cx, |search_bar, window, cx| {
2189            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2190            assert_eq!(
2191                editor.update(cx, |editor, cx| editor
2192                    .selections
2193                    .display_ranges(&editor.display_snapshot(cx))),
2194                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2195            );
2196        });
2197        search_bar.read_with(cx, |search_bar, _| {
2198            assert_eq!(search_bar.active_match_index, Some(0));
2199        });
2200
2201        // Park the cursor in between matches and ensure that going to the previous match
2202        // selects the closest match to the left of the cursor.
2203        editor.update_in(cx, |editor, window, cx| {
2204            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2205                s.select_display_ranges([
2206                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2207                ])
2208            });
2209        });
2210        search_bar.update_in(cx, |search_bar, window, cx| {
2211            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2212            assert_eq!(
2213                editor.update(cx, |editor, cx| editor
2214                    .selections
2215                    .display_ranges(&editor.display_snapshot(cx))),
2216                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2217            );
2218        });
2219        search_bar.read_with(cx, |search_bar, _| {
2220            assert_eq!(search_bar.active_match_index, Some(0));
2221        });
2222
2223        // Park the cursor in between matches and ensure that going to the next match
2224        // selects the closest match to the right of the cursor.
2225        editor.update_in(cx, |editor, window, cx| {
2226            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2227                s.select_display_ranges([
2228                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
2229                ])
2230            });
2231        });
2232        search_bar.update_in(cx, |search_bar, window, cx| {
2233            search_bar.select_next_match(&SelectNextMatch, window, cx);
2234            assert_eq!(
2235                editor.update(cx, |editor, cx| editor
2236                    .selections
2237                    .display_ranges(&editor.display_snapshot(cx))),
2238                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
2239            );
2240        });
2241        search_bar.read_with(cx, |search_bar, _| {
2242            assert_eq!(search_bar.active_match_index, Some(1));
2243        });
2244
2245        // Park the cursor after the last match and ensure that going to the previous match
2246        // selects the last match.
2247        editor.update_in(cx, |editor, window, cx| {
2248            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2249                s.select_display_ranges([
2250                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2251                ])
2252            });
2253        });
2254        search_bar.update_in(cx, |search_bar, window, cx| {
2255            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2256            assert_eq!(
2257                editor.update(cx, |editor, cx| editor
2258                    .selections
2259                    .display_ranges(&editor.display_snapshot(cx))),
2260                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2261            );
2262        });
2263        search_bar.read_with(cx, |search_bar, _| {
2264            assert_eq!(search_bar.active_match_index, Some(2));
2265        });
2266
2267        // Park the cursor after the last match and ensure that going to the next match
2268        // wraps around and selects the first match.
2269        editor.update_in(cx, |editor, window, cx| {
2270            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2271                s.select_display_ranges([
2272                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
2273                ])
2274            });
2275        });
2276        search_bar.update_in(cx, |search_bar, window, cx| {
2277            search_bar.select_next_match(&SelectNextMatch, window, cx);
2278            assert_eq!(
2279                editor.update(cx, |editor, cx| editor
2280                    .selections
2281                    .display_ranges(&editor.display_snapshot(cx))),
2282                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
2283            );
2284        });
2285        search_bar.read_with(cx, |search_bar, _| {
2286            assert_eq!(search_bar.active_match_index, Some(0));
2287        });
2288
2289        // Park the cursor before the first match and ensure that going to the previous match
2290        // wraps around and selects the last match.
2291        editor.update_in(cx, |editor, window, cx| {
2292            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2293                s.select_display_ranges([
2294                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2295                ])
2296            });
2297        });
2298        search_bar.update_in(cx, |search_bar, window, cx| {
2299            assert_eq!(search_bar.active_match_index, Some(0));
2300            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2301            assert_eq!(
2302                editor.update(cx, |editor, cx| editor
2303                    .selections
2304                    .display_ranges(&editor.display_snapshot(cx))),
2305                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
2306            );
2307        });
2308        search_bar.read_with(cx, |search_bar, _| {
2309            assert_eq!(search_bar.active_match_index, Some(2));
2310        });
2311    }
2312
2313    fn display_points_of(
2314        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
2315    ) -> Vec<Range<DisplayPoint>> {
2316        background_highlights
2317            .into_iter()
2318            .map(|(range, _)| range)
2319            .collect::<Vec<_>>()
2320    }
2321
2322    #[perf]
2323    #[gpui::test]
2324    async fn test_search_option_handling(cx: &mut TestAppContext) {
2325        let (editor, search_bar, cx) = init_test(cx);
2326
2327        // show with options should make current search case sensitive
2328        search_bar
2329            .update_in(cx, |search_bar, window, cx| {
2330                search_bar.show(window, cx);
2331                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2332            })
2333            .await
2334            .unwrap();
2335        editor.update_in(cx, |editor, window, cx| {
2336            assert_eq!(
2337                display_points_of(editor.all_text_background_highlights(window, cx)),
2338                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
2339            );
2340        });
2341
2342        // search_suggested should restore default options
2343        search_bar.update_in(cx, |search_bar, window, cx| {
2344            search_bar.search_suggested(None, window, cx);
2345            assert_eq!(search_bar.search_options, SearchOptions::NONE)
2346        });
2347
2348        // toggling a search option should update the defaults
2349        search_bar
2350            .update_in(cx, |search_bar, window, cx| {
2351                search_bar.search(
2352                    "regex",
2353                    Some(SearchOptions::CASE_SENSITIVE),
2354                    true,
2355                    window,
2356                    cx,
2357                )
2358            })
2359            .await
2360            .unwrap();
2361        search_bar.update_in(cx, |search_bar, window, cx| {
2362            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
2363        });
2364        let mut editor_notifications = cx.notifications(&editor);
2365        editor_notifications.next().await;
2366        editor.update_in(cx, |editor, window, cx| {
2367            assert_eq!(
2368                display_points_of(editor.all_text_background_highlights(window, cx)),
2369                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
2370            );
2371        });
2372
2373        // defaults should still include whole word
2374        search_bar.update_in(cx, |search_bar, window, cx| {
2375            search_bar.search_suggested(None, window, cx);
2376            assert_eq!(
2377                search_bar.search_options,
2378                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
2379            )
2380        });
2381    }
2382
2383    #[perf]
2384    #[gpui::test]
2385    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
2386        init_globals(cx);
2387        let buffer_text = r#"
2388        A regular expression (shortened as regex or regexp;[1] also referred to as
2389        rational expression[2][3]) is a sequence of characters that specifies a search
2390        pattern in text. Usually such patterns are used by string-searching algorithms
2391        for "find" or "find and replace" operations on strings, or for input validation.
2392        "#
2393        .unindent();
2394        let expected_query_matches_count = buffer_text
2395            .chars()
2396            .filter(|c| c.eq_ignore_ascii_case(&'a'))
2397            .count();
2398        assert!(
2399            expected_query_matches_count > 1,
2400            "Should pick a query with multiple results"
2401        );
2402        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2403        let window = cx.add_window(|_, _| gpui::Empty);
2404
2405        let editor = window.build_entity(cx, |window, cx| {
2406            Editor::for_buffer(buffer.clone(), None, window, cx)
2407        });
2408
2409        let search_bar = window.build_entity(cx, |window, cx| {
2410            let mut search_bar = BufferSearchBar::new(None, window, cx);
2411            search_bar.set_active_pane_item(Some(&editor), window, cx);
2412            search_bar.show(window, cx);
2413            search_bar
2414        });
2415
2416        window
2417            .update(cx, |_, window, cx| {
2418                search_bar.update(cx, |search_bar, cx| {
2419                    search_bar.search("a", None, true, window, cx)
2420                })
2421            })
2422            .unwrap()
2423            .await
2424            .unwrap();
2425        let initial_selections = window
2426            .update(cx, |_, window, cx| {
2427                search_bar.update(cx, |search_bar, cx| {
2428                    let handle = search_bar.query_editor.focus_handle(cx);
2429                    window.focus(&handle, cx);
2430                    search_bar.activate_current_match(window, cx);
2431                });
2432                assert!(
2433                    !editor.read(cx).is_focused(window),
2434                    "Initially, the editor should not be focused"
2435                );
2436                let initial_selections = editor.update(cx, |editor, cx| {
2437                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2438                    assert_eq!(
2439                        initial_selections.len(), 1,
2440                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2441                    );
2442                    initial_selections
2443                });
2444                search_bar.update(cx, |search_bar, cx| {
2445                    assert_eq!(search_bar.active_match_index, Some(0));
2446                    let handle = search_bar.query_editor.focus_handle(cx);
2447                    window.focus(&handle, cx);
2448                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2449                });
2450                assert!(
2451                    editor.read(cx).is_focused(window),
2452                    "Should focus editor after successful SelectAllMatches"
2453                );
2454                search_bar.update(cx, |search_bar, cx| {
2455                    let all_selections =
2456                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2457                    assert_eq!(
2458                        all_selections.len(),
2459                        expected_query_matches_count,
2460                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2461                    );
2462                    assert_eq!(
2463                        search_bar.active_match_index,
2464                        Some(0),
2465                        "Match index should not change after selecting all matches"
2466                    );
2467                });
2468
2469                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2470                initial_selections
2471            }).unwrap();
2472
2473        window
2474            .update(cx, |_, window, cx| {
2475                assert!(
2476                    editor.read(cx).is_focused(window),
2477                    "Should still have editor focused after SelectNextMatch"
2478                );
2479                search_bar.update(cx, |search_bar, cx| {
2480                    let all_selections = editor.update(cx, |editor, cx| {
2481                        editor
2482                            .selections
2483                            .display_ranges(&editor.display_snapshot(cx))
2484                    });
2485                    assert_eq!(
2486                        all_selections.len(),
2487                        1,
2488                        "On next match, should deselect items and select the next match"
2489                    );
2490                    assert_ne!(
2491                        all_selections, initial_selections,
2492                        "Next match should be different from the first selection"
2493                    );
2494                    assert_eq!(
2495                        search_bar.active_match_index,
2496                        Some(1),
2497                        "Match index should be updated to the next one"
2498                    );
2499                    let handle = search_bar.query_editor.focus_handle(cx);
2500                    window.focus(&handle, cx);
2501                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2502                });
2503            })
2504            .unwrap();
2505        window
2506            .update(cx, |_, window, cx| {
2507                assert!(
2508                    editor.read(cx).is_focused(window),
2509                    "Should focus editor after successful SelectAllMatches"
2510                );
2511                search_bar.update(cx, |search_bar, cx| {
2512                    let all_selections =
2513                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2514                    assert_eq!(
2515                    all_selections.len(),
2516                    expected_query_matches_count,
2517                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2518                );
2519                    assert_eq!(
2520                        search_bar.active_match_index,
2521                        Some(1),
2522                        "Match index should not change after selecting all matches"
2523                    );
2524                });
2525                search_bar.update(cx, |search_bar, cx| {
2526                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2527                });
2528            })
2529            .unwrap();
2530        let last_match_selections = window
2531            .update(cx, |_, window, cx| {
2532                assert!(
2533                    editor.read(cx).is_focused(window),
2534                    "Should still have editor focused after SelectPreviousMatch"
2535                );
2536
2537                search_bar.update(cx, |search_bar, cx| {
2538                    let all_selections = editor.update(cx, |editor, cx| {
2539                        editor
2540                            .selections
2541                            .display_ranges(&editor.display_snapshot(cx))
2542                    });
2543                    assert_eq!(
2544                        all_selections.len(),
2545                        1,
2546                        "On previous match, should deselect items and select the previous item"
2547                    );
2548                    assert_eq!(
2549                        all_selections, initial_selections,
2550                        "Previous match should be the same as the first selection"
2551                    );
2552                    assert_eq!(
2553                        search_bar.active_match_index,
2554                        Some(0),
2555                        "Match index should be updated to the previous one"
2556                    );
2557                    all_selections
2558                })
2559            })
2560            .unwrap();
2561
2562        window
2563            .update(cx, |_, window, cx| {
2564                search_bar.update(cx, |search_bar, cx| {
2565                    let handle = search_bar.query_editor.focus_handle(cx);
2566                    window.focus(&handle, cx);
2567                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2568                })
2569            })
2570            .unwrap()
2571            .await
2572            .unwrap();
2573        window
2574            .update(cx, |_, window, cx| {
2575                search_bar.update(cx, |search_bar, cx| {
2576                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2577                });
2578                assert!(
2579                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2580                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2581                );
2582                search_bar.update(cx, |search_bar, cx| {
2583                    let all_selections =
2584                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2585                    assert_eq!(
2586                        all_selections, last_match_selections,
2587                        "Should not select anything new if there are no matches"
2588                    );
2589                    assert!(
2590                        search_bar.active_match_index.is_none(),
2591                        "For no matches, there should be no active match index"
2592                    );
2593                });
2594            })
2595            .unwrap();
2596    }
2597
2598    #[perf]
2599    #[gpui::test]
2600    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2601        init_globals(cx);
2602        let buffer_text = r#"
2603        self.buffer.update(cx, |buffer, cx| {
2604            buffer.edit(
2605                edits,
2606                Some(AutoindentMode::Block {
2607                    original_indent_columns,
2608                }),
2609                cx,
2610            )
2611        });
2612
2613        this.buffer.update(cx, |buffer, cx| {
2614            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2615        });
2616        "#
2617        .unindent();
2618        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2619        let cx = cx.add_empty_window();
2620
2621        let editor =
2622            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2623
2624        let search_bar = cx.new_window_entity(|window, cx| {
2625            let mut search_bar = BufferSearchBar::new(None, window, cx);
2626            search_bar.set_active_pane_item(Some(&editor), window, cx);
2627            search_bar.show(window, cx);
2628            search_bar
2629        });
2630
2631        search_bar
2632            .update_in(cx, |search_bar, window, cx| {
2633                search_bar.search(
2634                    "edit\\(",
2635                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2636                    true,
2637                    window,
2638                    cx,
2639                )
2640            })
2641            .await
2642            .unwrap();
2643
2644        search_bar.update_in(cx, |search_bar, window, cx| {
2645            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2646        });
2647        search_bar.update(cx, |_, cx| {
2648            let all_selections = editor.update(cx, |editor, cx| {
2649                editor
2650                    .selections
2651                    .display_ranges(&editor.display_snapshot(cx))
2652            });
2653            assert_eq!(
2654                all_selections.len(),
2655                2,
2656                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2657            );
2658        });
2659
2660        search_bar
2661            .update_in(cx, |search_bar, window, cx| {
2662                search_bar.search(
2663                    "edit(",
2664                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2665                    true,
2666                    window,
2667                    cx,
2668                )
2669            })
2670            .await
2671            .unwrap();
2672
2673        search_bar.update_in(cx, |search_bar, window, cx| {
2674            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2675        });
2676        search_bar.update(cx, |_, cx| {
2677            let all_selections = editor.update(cx, |editor, cx| {
2678                editor
2679                    .selections
2680                    .display_ranges(&editor.display_snapshot(cx))
2681            });
2682            assert_eq!(
2683                all_selections.len(),
2684                2,
2685                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2686            );
2687        });
2688    }
2689
2690    #[perf]
2691    #[gpui::test]
2692    async fn test_search_query_history(cx: &mut TestAppContext) {
2693        let (_editor, search_bar, cx) = init_test(cx);
2694
2695        // Add 3 search items into the history.
2696        search_bar
2697            .update_in(cx, |search_bar, window, cx| {
2698                search_bar.search("a", None, true, window, cx)
2699            })
2700            .await
2701            .unwrap();
2702        search_bar
2703            .update_in(cx, |search_bar, window, cx| {
2704                search_bar.search("b", None, true, window, cx)
2705            })
2706            .await
2707            .unwrap();
2708        search_bar
2709            .update_in(cx, |search_bar, window, cx| {
2710                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2711            })
2712            .await
2713            .unwrap();
2714        // Ensure that the latest search is active.
2715        search_bar.update(cx, |search_bar, cx| {
2716            assert_eq!(search_bar.query(cx), "c");
2717            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2718        });
2719
2720        // Next history query after the latest should preserve the current query.
2721        search_bar.update_in(cx, |search_bar, window, cx| {
2722            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2723        });
2724        cx.background_executor.run_until_parked();
2725        search_bar.update(cx, |search_bar, cx| {
2726            assert_eq!(search_bar.query(cx), "c");
2727            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2728        });
2729        search_bar.update_in(cx, |search_bar, window, cx| {
2730            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2731        });
2732        cx.background_executor.run_until_parked();
2733        search_bar.update(cx, |search_bar, cx| {
2734            assert_eq!(search_bar.query(cx), "c");
2735            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2736        });
2737
2738        // Previous query should navigate backwards through history.
2739        search_bar.update_in(cx, |search_bar, window, cx| {
2740            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2741        });
2742        cx.background_executor.run_until_parked();
2743        search_bar.update(cx, |search_bar, cx| {
2744            assert_eq!(search_bar.query(cx), "b");
2745            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2746        });
2747
2748        // Further previous items should go over the history in reverse order.
2749        search_bar.update_in(cx, |search_bar, window, cx| {
2750            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2751        });
2752        cx.background_executor.run_until_parked();
2753        search_bar.update(cx, |search_bar, cx| {
2754            assert_eq!(search_bar.query(cx), "a");
2755            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2756        });
2757
2758        // Previous items should never go behind the first history item.
2759        search_bar.update_in(cx, |search_bar, window, cx| {
2760            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2761        });
2762        cx.background_executor.run_until_parked();
2763        search_bar.update(cx, |search_bar, cx| {
2764            assert_eq!(search_bar.query(cx), "a");
2765            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2766        });
2767        search_bar.update_in(cx, |search_bar, window, cx| {
2768            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2769        });
2770        cx.background_executor.run_until_parked();
2771        search_bar.update(cx, |search_bar, cx| {
2772            assert_eq!(search_bar.query(cx), "a");
2773            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2774        });
2775
2776        // Next items should go over the history in the original order.
2777        search_bar.update_in(cx, |search_bar, window, cx| {
2778            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2779        });
2780        cx.background_executor.run_until_parked();
2781        search_bar.update(cx, |search_bar, cx| {
2782            assert_eq!(search_bar.query(cx), "b");
2783            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2784        });
2785
2786        search_bar
2787            .update_in(cx, |search_bar, window, cx| {
2788                search_bar.search("ba", None, true, window, cx)
2789            })
2790            .await
2791            .unwrap();
2792        search_bar.update(cx, |search_bar, cx| {
2793            assert_eq!(search_bar.query(cx), "ba");
2794            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2795        });
2796
2797        // New search input should add another entry to history and move the selection to the end of the history.
2798        search_bar.update_in(cx, |search_bar, window, cx| {
2799            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2800        });
2801        cx.background_executor.run_until_parked();
2802        search_bar.update(cx, |search_bar, cx| {
2803            assert_eq!(search_bar.query(cx), "c");
2804            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2805        });
2806        search_bar.update_in(cx, |search_bar, window, cx| {
2807            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2808        });
2809        cx.background_executor.run_until_parked();
2810        search_bar.update(cx, |search_bar, cx| {
2811            assert_eq!(search_bar.query(cx), "b");
2812            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2813        });
2814        search_bar.update_in(cx, |search_bar, window, cx| {
2815            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2816        });
2817        cx.background_executor.run_until_parked();
2818        search_bar.update(cx, |search_bar, cx| {
2819            assert_eq!(search_bar.query(cx), "c");
2820            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2821        });
2822        search_bar.update_in(cx, |search_bar, window, cx| {
2823            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2824        });
2825        cx.background_executor.run_until_parked();
2826        search_bar.update(cx, |search_bar, cx| {
2827            assert_eq!(search_bar.query(cx), "ba");
2828            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2829        });
2830        search_bar.update_in(cx, |search_bar, window, cx| {
2831            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2832        });
2833        cx.background_executor.run_until_parked();
2834        search_bar.update(cx, |search_bar, cx| {
2835            assert_eq!(search_bar.query(cx), "ba");
2836            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2837        });
2838    }
2839
2840    #[perf]
2841    #[gpui::test]
2842    async fn test_search_query_history_autoscroll(cx: &mut TestAppContext) {
2843        let (_editor, search_bar, cx) = init_test(cx);
2844
2845        // Add a long multi-line query that exceeds the editor's max
2846        // visible height (4 lines), then a short query.
2847        let long_query = "line1\nline2\nline3\nline4\nline5\nline6";
2848        search_bar
2849            .update_in(cx, |search_bar, window, cx| {
2850                search_bar.search(long_query, None, true, window, cx)
2851            })
2852            .await
2853            .unwrap();
2854        search_bar
2855            .update_in(cx, |search_bar, window, cx| {
2856                search_bar.search("short", None, true, window, cx)
2857            })
2858            .await
2859            .unwrap();
2860
2861        // Navigate back to the long entry. Since "short" is single-line,
2862        // the history navigation is allowed.
2863        search_bar.update_in(cx, |search_bar, window, cx| {
2864            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2865        });
2866        cx.background_executor.run_until_parked();
2867        search_bar.update(cx, |search_bar, cx| {
2868            assert_eq!(search_bar.query(cx), long_query);
2869        });
2870
2871        // The cursor should be scrolled into view despite the content
2872        // exceeding the editor's max visible height.
2873        search_bar.update_in(cx, |search_bar, window, cx| {
2874            let snapshot = search_bar
2875                .query_editor
2876                .update(cx, |editor, cx| editor.snapshot(window, cx));
2877            let cursor_row = search_bar
2878                .query_editor
2879                .read(cx)
2880                .selections
2881                .newest_display(&snapshot)
2882                .head()
2883                .row();
2884            let scroll_top = search_bar
2885                .query_editor
2886                .update(cx, |editor, cx| editor.scroll_position(cx).y);
2887            let visible_lines = search_bar
2888                .query_editor
2889                .read(cx)
2890                .visible_line_count()
2891                .unwrap_or(0.0);
2892            let scroll_bottom = scroll_top + visible_lines;
2893            assert!(
2894                (cursor_row.0 as f64) < scroll_bottom,
2895                "cursor row {cursor_row:?} should be visible (scroll range {scroll_top}..{scroll_bottom})"
2896            );
2897        });
2898    }
2899
2900    #[perf]
2901    #[gpui::test]
2902    async fn test_replace_simple(cx: &mut TestAppContext) {
2903        let (editor, search_bar, cx) = init_test(cx);
2904
2905        search_bar
2906            .update_in(cx, |search_bar, window, cx| {
2907                search_bar.search("expression", None, true, window, cx)
2908            })
2909            .await
2910            .unwrap();
2911
2912        search_bar.update_in(cx, |search_bar, window, cx| {
2913            search_bar.replacement_editor.update(cx, |editor, cx| {
2914                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2915                editor.set_text("expr$1", window, cx);
2916            });
2917            search_bar.replace_all(&ReplaceAll, window, cx)
2918        });
2919        assert_eq!(
2920            editor.read_with(cx, |this, cx| { this.text(cx) }),
2921            r#"
2922        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2923        rational expr$1[2][3]) is a sequence of characters that specifies a search
2924        pattern in text. Usually such patterns are used by string-searching algorithms
2925        for "find" or "find and replace" operations on strings, or for input validation.
2926        "#
2927            .unindent()
2928        );
2929
2930        // Search for word boundaries and replace just a single one.
2931        search_bar
2932            .update_in(cx, |search_bar, window, cx| {
2933                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2934            })
2935            .await
2936            .unwrap();
2937
2938        search_bar.update_in(cx, |search_bar, window, cx| {
2939            search_bar.replacement_editor.update(cx, |editor, cx| {
2940                editor.set_text("banana", window, cx);
2941            });
2942            search_bar.replace_next(&ReplaceNext, window, cx)
2943        });
2944        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2945        assert_eq!(
2946            editor.read_with(cx, |this, cx| { this.text(cx) }),
2947            r#"
2948        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2949        rational expr$1[2][3]) is a sequence of characters that specifies a search
2950        pattern in text. Usually such patterns are used by string-searching algorithms
2951        for "find" or "find and replace" operations on strings, or for input validation.
2952        "#
2953            .unindent()
2954        );
2955        // Let's turn on regex mode.
2956        search_bar
2957            .update_in(cx, |search_bar, window, cx| {
2958                search_bar.search(
2959                    "\\[([^\\]]+)\\]",
2960                    Some(SearchOptions::REGEX),
2961                    true,
2962                    window,
2963                    cx,
2964                )
2965            })
2966            .await
2967            .unwrap();
2968        search_bar.update_in(cx, |search_bar, window, cx| {
2969            search_bar.replacement_editor.update(cx, |editor, cx| {
2970                editor.set_text("${1}number", window, cx);
2971            });
2972            search_bar.replace_all(&ReplaceAll, window, cx)
2973        });
2974        assert_eq!(
2975            editor.read_with(cx, |this, cx| { this.text(cx) }),
2976            r#"
2977        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2978        rational expr$12number3number) is a sequence of characters that specifies a search
2979        pattern in text. Usually such patterns are used by string-searching algorithms
2980        for "find" or "find and replace" operations on strings, or for input validation.
2981        "#
2982            .unindent()
2983        );
2984        // Now with a whole-word twist.
2985        search_bar
2986            .update_in(cx, |search_bar, window, cx| {
2987                search_bar.search(
2988                    "a\\w+s",
2989                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2990                    true,
2991                    window,
2992                    cx,
2993                )
2994            })
2995            .await
2996            .unwrap();
2997        search_bar.update_in(cx, |search_bar, window, cx| {
2998            search_bar.replacement_editor.update(cx, |editor, cx| {
2999                editor.set_text("things", window, cx);
3000            });
3001            search_bar.replace_all(&ReplaceAll, window, cx)
3002        });
3003        // The only word affected by this edit should be `algorithms`, even though there's a bunch
3004        // of words in this text that would match this regex if not for WHOLE_WORD.
3005        assert_eq!(
3006            editor.read_with(cx, |this, cx| { this.text(cx) }),
3007            r#"
3008        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
3009        rational expr$12number3number) is a sequence of characters that specifies a search
3010        pattern in text. Usually such patterns are used by string-searching things
3011        for "find" or "find and replace" operations on strings, or for input validation.
3012        "#
3013            .unindent()
3014        );
3015    }
3016
3017    #[gpui::test]
3018    async fn test_replace_focus(cx: &mut TestAppContext) {
3019        let (editor, search_bar, cx) = init_test(cx);
3020
3021        editor.update_in(cx, |editor, window, cx| {
3022            editor.set_text("What a bad day!", window, cx)
3023        });
3024
3025        search_bar
3026            .update_in(cx, |search_bar, window, cx| {
3027                search_bar.search("bad", None, true, window, cx)
3028            })
3029            .await
3030            .unwrap();
3031
3032        // Calling `toggle_replace` in the search bar ensures that the "Replace
3033        // *" buttons are rendered, so we can then simulate clicking the
3034        // buttons.
3035        search_bar.update_in(cx, |search_bar, window, cx| {
3036            search_bar.toggle_replace(&ToggleReplace, window, cx)
3037        });
3038
3039        search_bar.update_in(cx, |search_bar, window, cx| {
3040            search_bar.replacement_editor.update(cx, |editor, cx| {
3041                editor.set_text("great", window, cx);
3042            });
3043        });
3044
3045        // Focus on the editor instead of the search bar, as we want to ensure
3046        // that pressing the "Replace Next Match" button will work, even if the
3047        // search bar is not focused.
3048        cx.focus(&editor);
3049
3050        // We'll not simulate clicking the "Replace Next Match " button, asserting that
3051        // the replacement was done.
3052        let button_bounds = cx
3053            .debug_bounds("ICON-ReplaceNext")
3054            .expect("'Replace Next Match' button should be visible");
3055        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
3056
3057        assert_eq!(
3058            editor.read_with(cx, |editor, cx| editor.text(cx)),
3059            "What a great day!"
3060        );
3061    }
3062
3063    struct ReplacementTestParams<'a> {
3064        editor: &'a Entity<Editor>,
3065        search_bar: &'a Entity<BufferSearchBar>,
3066        cx: &'a mut VisualTestContext,
3067        search_text: &'static str,
3068        search_options: Option<SearchOptions>,
3069        replacement_text: &'static str,
3070        replace_all: bool,
3071        expected_text: String,
3072    }
3073
3074    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
3075        options
3076            .search_bar
3077            .update_in(options.cx, |search_bar, window, cx| {
3078                if let Some(options) = options.search_options {
3079                    search_bar.set_search_options(options, cx);
3080                }
3081                search_bar.search(
3082                    options.search_text,
3083                    options.search_options,
3084                    true,
3085                    window,
3086                    cx,
3087                )
3088            })
3089            .await
3090            .unwrap();
3091
3092        options
3093            .search_bar
3094            .update_in(options.cx, |search_bar, window, cx| {
3095                search_bar.replacement_editor.update(cx, |editor, cx| {
3096                    editor.set_text(options.replacement_text, window, cx);
3097                });
3098
3099                if options.replace_all {
3100                    search_bar.replace_all(&ReplaceAll, window, cx)
3101                } else {
3102                    search_bar.replace_next(&ReplaceNext, window, cx)
3103                }
3104            });
3105
3106        assert_eq!(
3107            options
3108                .editor
3109                .read_with(options.cx, |this, cx| { this.text(cx) }),
3110            options.expected_text
3111        );
3112    }
3113
3114    #[perf]
3115    #[gpui::test]
3116    async fn test_replace_special_characters(cx: &mut TestAppContext) {
3117        let (editor, search_bar, cx) = init_test(cx);
3118
3119        run_replacement_test(ReplacementTestParams {
3120            editor: &editor,
3121            search_bar: &search_bar,
3122            cx,
3123            search_text: "expression",
3124            search_options: None,
3125            replacement_text: r"\n",
3126            replace_all: true,
3127            expected_text: r#"
3128            A regular \n (shortened as regex or regexp;[1] also referred to as
3129            rational \n[2][3]) is a sequence of characters that specifies a search
3130            pattern in text. Usually such patterns are used by string-searching algorithms
3131            for "find" or "find and replace" operations on strings, or for input validation.
3132            "#
3133            .unindent(),
3134        })
3135        .await;
3136
3137        run_replacement_test(ReplacementTestParams {
3138            editor: &editor,
3139            search_bar: &search_bar,
3140            cx,
3141            search_text: "or",
3142            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
3143            replacement_text: r"\\\n\\\\",
3144            replace_all: false,
3145            expected_text: r#"
3146            A regular \n (shortened as regex \
3147            \\ regexp;[1] also referred to as
3148            rational \n[2][3]) is a sequence of characters that specifies a search
3149            pattern in text. Usually such patterns are used by string-searching algorithms
3150            for "find" or "find and replace" operations on strings, or for input validation.
3151            "#
3152            .unindent(),
3153        })
3154        .await;
3155
3156        run_replacement_test(ReplacementTestParams {
3157            editor: &editor,
3158            search_bar: &search_bar,
3159            cx,
3160            search_text: r"(that|used) ",
3161            search_options: Some(SearchOptions::REGEX),
3162            replacement_text: r"$1\n",
3163            replace_all: true,
3164            expected_text: r#"
3165            A regular \n (shortened as regex \
3166            \\ regexp;[1] also referred to as
3167            rational \n[2][3]) is a sequence of characters that
3168            specifies a search
3169            pattern in text. Usually such patterns are used
3170            by string-searching algorithms
3171            for "find" or "find and replace" operations on strings, or for input validation.
3172            "#
3173            .unindent(),
3174        })
3175        .await;
3176    }
3177
3178    #[gpui::test]
3179    async fn test_deploy_replace_focuses_replacement_editor(cx: &mut TestAppContext) {
3180        init_globals(cx);
3181        let (editor, search_bar, cx) = init_test(cx);
3182
3183        editor.update_in(cx, |editor, window, cx| {
3184            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3185                s.select_display_ranges([
3186                    DisplayPoint::new(DisplayRow(0), 8)..DisplayPoint::new(DisplayRow(0), 16)
3187                ])
3188            });
3189        });
3190
3191        search_bar.update_in(cx, |search_bar, window, cx| {
3192            search_bar.deploy(
3193                &Deploy {
3194                    focus: true,
3195                    replace_enabled: true,
3196                    selection_search_enabled: false,
3197                },
3198                None,
3199                window,
3200                cx,
3201            );
3202        });
3203        cx.run_until_parked();
3204
3205        search_bar.update_in(cx, |search_bar, window, cx| {
3206            assert!(
3207                search_bar
3208                    .replacement_editor
3209                    .focus_handle(cx)
3210                    .is_focused(window),
3211                "replacement editor should be focused when deploying replace with a selection",
3212            );
3213            assert!(
3214                !search_bar.query_editor.focus_handle(cx).is_focused(window),
3215                "search editor should not be focused when replacement editor is focused",
3216            );
3217        });
3218    }
3219
3220    #[gpui::test]
3221    async fn test_toggle_soft_wrap_relays_to_searched_editor(cx: &mut TestAppContext) {
3222        init_globals(cx);
3223        let (editor, search_bar, cx) = init_test(cx);
3224
3225        search_bar.update_in(cx, |search_bar, window, cx| {
3226            search_bar.deploy(
3227                &Deploy {
3228                    focus: true,
3229                    replace_enabled: false,
3230                    selection_search_enabled: false,
3231                },
3232                None,
3233                window,
3234                cx,
3235            );
3236        });
3237        cx.run_until_parked();
3238
3239        search_bar.update_in(cx, |search_bar, window, cx| {
3240            assert!(
3241                search_bar.query_editor.focus_handle(cx).is_focused(window),
3242                "query editor should be focused after deploying the search bar",
3243            );
3244        });
3245        editor.update_in(cx, |editor, _, cx| {
3246            assert!(
3247                matches!(editor.soft_wrap_mode(cx), SoftWrap::None),
3248                "soft wrap should be disabled initially",
3249            );
3250        });
3251
3252        cx.dispatch_action(ToggleSoftWrap);
3253        cx.run_until_parked();
3254
3255        editor.update_in(cx, |editor, _, cx| {
3256            assert!(
3257                matches!(editor.soft_wrap_mode(cx), SoftWrap::EditorWidth),
3258                "toggling soft wrap while the search bar is focused should affect the searched editor",
3259            );
3260        });
3261        search_bar.update_in(cx, |search_bar, window, cx| {
3262            assert!(
3263                search_bar.query_editor.focus_handle(cx).is_focused(window),
3264                "focus should stay in the search bar",
3265            );
3266        });
3267    }
3268
3269    #[cfg(target_os = "macos")]
3270    #[gpui::test]
3271    async fn test_cmd_e_then_cmd_g_uses_selection_for_find(cx: &mut TestAppContext) {
3272        init_globals(cx);
3273        let app_state = cx.update(AppState::test);
3274        let project = Project::test(app_state.fs.clone(), [], cx).await;
3275        let buffer = cx.new(|cx| {
3276            Buffer::local(
3277                r#"
3278                dad
3279                cat
3280                mom
3281                dog
3282                dog
3283                cat
3284                dad
3285                mom
3286                "#
3287                .unindent(),
3288                cx,
3289            )
3290        });
3291        let multibuffer = cx.update(|cx| MultiBuffer::build_from_buffer(buffer, cx));
3292        let mut editor = None;
3293        let mut search_bar = None;
3294
3295        let window = cx.add_window(|window, cx| {
3296            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
3297                "keymaps/default-macos.json",
3298                cx,
3299            )
3300            .unwrap();
3301            cx.bind_keys(default_key_bindings);
3302            let workspace = cx.new(|cx| Workspace::test_new(project.clone(), window, cx));
3303            let multi_workspace = MultiWorkspace::new(workspace.clone(), window, cx);
3304            let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx));
3305            workspace.update(cx, |workspace, cx| {
3306                workspace.active_pane().update(cx, |pane, cx| {
3307                    pane.toolbar().update(cx, |toolbar, cx| {
3308                        toolbar.add_item(buffer_search_bar.clone(), window, cx);
3309                    });
3310                });
3311            });
3312            let editor_handle = cx.new(|cx| {
3313                Editor::new(
3314                    editor::EditorMode::full(),
3315                    multibuffer.clone(),
3316                    Some(project.clone()),
3317                    window,
3318                    cx,
3319                )
3320            });
3321            workspace.update(cx, |workspace, cx| {
3322                workspace.add_item_to_center(Box::new(editor_handle.clone()), window, cx);
3323            });
3324            window.focus(&editor_handle.focus_handle(cx), cx);
3325            search_bar = Some(buffer_search_bar);
3326            editor = Some(editor_handle);
3327            multi_workspace
3328        });
3329        let cx = VisualTestContext::from_window(*window, cx).into_mut();
3330        let editor = editor.unwrap();
3331        let search_bar = search_bar.unwrap();
3332
3333        editor.update_in(cx, |editor, window, cx| {
3334            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
3335                selections.select_display_ranges([
3336                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3337                ]);
3338            });
3339        });
3340
3341        cx.simulate_keystrokes("cmd-e");
3342
3343        search_bar.read_with(cx, |search_bar, cx| {
3344            assert_eq!(search_bar.query(cx), "dog");
3345            assert_eq!(search_bar.active_match_index, Some(0));
3346        });
3347        cx.read(|cx| {
3348            assert_eq!(
3349                cx.read_from_find_pasteboard().and_then(|item| item.text()),
3350                Some("dog".to_string())
3351            );
3352        });
3353
3354        cx.simulate_keystrokes("cmd-g");
3355        assert_eq!(
3356            editor.update(cx, |editor, cx| editor
3357                .selections
3358                .display_ranges(&editor.display_snapshot(cx))),
3359            [DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 3)]
3360        );
3361
3362        editor.update_in(cx, |editor, window, cx| {
3363            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
3364                selections.select_display_ranges([
3365                    DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)
3366                ]);
3367            });
3368        });
3369
3370        cx.simulate_keystrokes("cmd-e");
3371
3372        search_bar.read_with(cx, |search_bar, cx| {
3373            assert_eq!(search_bar.query(cx), "cat");
3374            assert_eq!(search_bar.active_match_index, Some(0));
3375        });
3376        cx.read(|cx| {
3377            assert_eq!(
3378                cx.read_from_find_pasteboard().and_then(|item| item.text()),
3379                Some("cat".to_string())
3380            );
3381        });
3382
3383        cx.simulate_keystrokes("cmd-g");
3384        assert_eq!(
3385            editor.update(cx, |editor, cx| editor
3386                .selections
3387                .display_ranges(&editor.display_snapshot(cx))),
3388            [DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 3)]
3389        );
3390    }
3391
3392    #[perf]
3393    #[gpui::test]
3394    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
3395        cx: &mut TestAppContext,
3396    ) {
3397        init_globals(cx);
3398        let buffer = cx.new(|cx| {
3399            Buffer::local(
3400                r#"
3401                aaa bbb aaa ccc
3402                aaa bbb aaa ccc
3403                aaa bbb aaa ccc
3404                aaa bbb aaa ccc
3405                aaa bbb aaa ccc
3406                aaa bbb aaa ccc
3407                "#
3408                .unindent(),
3409                cx,
3410            )
3411        });
3412        let cx = cx.add_empty_window();
3413        let editor =
3414            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
3415
3416        let search_bar = cx.new_window_entity(|window, cx| {
3417            let mut search_bar = BufferSearchBar::new(None, window, cx);
3418            search_bar.set_active_pane_item(Some(&editor), window, cx);
3419            search_bar.show(window, cx);
3420            search_bar
3421        });
3422
3423        editor.update_in(cx, |editor, window, cx| {
3424            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3425                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
3426            })
3427        });
3428
3429        search_bar.update_in(cx, |search_bar, window, cx| {
3430            let deploy = Deploy {
3431                focus: true,
3432                replace_enabled: false,
3433                selection_search_enabled: true,
3434            };
3435            search_bar.deploy(&deploy, None, window, cx);
3436        });
3437
3438        cx.run_until_parked();
3439
3440        search_bar
3441            .update_in(cx, |search_bar, window, cx| {
3442                search_bar.search("aaa", None, true, window, cx)
3443            })
3444            .await
3445            .unwrap();
3446
3447        editor.update(cx, |editor, cx| {
3448            assert_eq!(
3449                editor.search_background_highlights(cx),
3450                &[
3451                    Point::new(1, 0)..Point::new(1, 3),
3452                    Point::new(1, 8)..Point::new(1, 11),
3453                    Point::new(2, 0)..Point::new(2, 3),
3454                ]
3455            );
3456        });
3457    }
3458
3459    #[perf]
3460    #[gpui::test]
3461    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
3462        cx: &mut TestAppContext,
3463    ) {
3464        init_globals(cx);
3465        let text = r#"
3466            aaa bbb aaa ccc
3467            aaa bbb aaa ccc
3468            aaa bbb aaa ccc
3469            aaa bbb aaa ccc
3470            aaa bbb aaa ccc
3471            aaa bbb aaa ccc
3472
3473            aaa bbb aaa ccc
3474            aaa bbb aaa ccc
3475            aaa bbb aaa ccc
3476            aaa bbb aaa ccc
3477            aaa bbb aaa ccc
3478            aaa bbb aaa ccc
3479            "#
3480        .unindent();
3481
3482        let cx = cx.add_empty_window();
3483        let editor = cx.new_window_entity(|window, cx| {
3484            let multibuffer = MultiBuffer::build_multi(
3485                [
3486                    (
3487                        &text,
3488                        vec![
3489                            Point::new(0, 0)..Point::new(2, 0),
3490                            Point::new(4, 0)..Point::new(5, 0),
3491                        ],
3492                    ),
3493                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
3494                ],
3495                cx,
3496            );
3497            Editor::for_multibuffer(multibuffer, None, window, cx)
3498        });
3499
3500        let search_bar = cx.new_window_entity(|window, cx| {
3501            let mut search_bar = BufferSearchBar::new(None, window, cx);
3502            search_bar.set_active_pane_item(Some(&editor), window, cx);
3503            search_bar.show(window, cx);
3504            search_bar
3505        });
3506
3507        editor.update_in(cx, |editor, window, cx| {
3508            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3509                s.select_ranges(vec![
3510                    Point::new(1, 0)..Point::new(1, 4),
3511                    Point::new(5, 3)..Point::new(6, 4),
3512                ])
3513            })
3514        });
3515
3516        search_bar.update_in(cx, |search_bar, window, cx| {
3517            let deploy = Deploy {
3518                focus: true,
3519                replace_enabled: false,
3520                selection_search_enabled: true,
3521            };
3522            search_bar.deploy(&deploy, None, window, cx);
3523        });
3524
3525        cx.run_until_parked();
3526
3527        search_bar
3528            .update_in(cx, |search_bar, window, cx| {
3529                search_bar.search("aaa", None, true, window, cx)
3530            })
3531            .await
3532            .unwrap();
3533
3534        editor.update(cx, |editor, cx| {
3535            assert_eq!(
3536                editor.search_background_highlights(cx),
3537                &[
3538                    Point::new(1, 0)..Point::new(1, 3),
3539                    Point::new(5, 8)..Point::new(5, 11),
3540                    Point::new(6, 0)..Point::new(6, 3),
3541                ]
3542            );
3543        });
3544    }
3545
3546    #[perf]
3547    #[gpui::test]
3548    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
3549        let (editor, search_bar, cx) = init_test(cx);
3550
3551        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3552            search_bar.set_active_pane_item(Some(&editor), window, cx)
3553        });
3554
3555        assert_eq!(initial_location, ToolbarItemLocation::Secondary);
3556
3557        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3558
3559        search_bar.update_in(cx, |search_bar, window, cx| {
3560            search_bar.dismiss(&Dismiss, window, cx);
3561        });
3562
3563        assert_eq!(
3564            events.try_recv().unwrap(),
3565            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3566        );
3567
3568        search_bar.update_in(cx, |search_bar, window, cx| {
3569            search_bar.show(window, cx);
3570        });
3571
3572        assert_eq!(
3573            events.try_recv().unwrap(),
3574            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3575        );
3576    }
3577
3578    #[perf]
3579    #[gpui::test]
3580    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
3581        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3582
3583        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3584            search_bar.set_active_pane_item(Some(&editor), window, cx)
3585        });
3586
3587        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);
3588
3589        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3590
3591        search_bar.update_in(cx, |search_bar, window, cx| {
3592            search_bar.dismiss(&Dismiss, window, cx);
3593        });
3594
3595        assert_eq!(
3596            events.try_recv().unwrap(),
3597            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3598        );
3599
3600        search_bar.update_in(cx, |search_bar, window, cx| {
3601            search_bar.show(window, cx);
3602        });
3603
3604        assert_eq!(
3605            events.try_recv().unwrap(),
3606            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
3607        );
3608    }
3609
3610    #[perf]
3611    #[gpui::test]
3612    async fn test_no_expand_collapse_option_when_item_is_not_buffer_backed(
3613        cx: &mut TestAppContext,
3614    ) {
3615        // Items that are backed by neither a singleton buffer nor a
3616        // multibuffer, for example, the LSP log view, report
3617        // `ItemBufferKind::None` and must not show the expand/collapse all
3618        // files button.
3619
3620        // A searchable item that reports `ItemBufferKind::None`, like the LSP
3621        // log view.
3622        struct NonBufferSearchableItem {
3623            focus_handle: FocusHandle,
3624        }
3625
3626        impl EventEmitter<()> for NonBufferSearchableItem {}
3627        impl EventEmitter<SearchEvent> for NonBufferSearchableItem {}
3628
3629        impl Focusable for NonBufferSearchableItem {
3630            fn focus_handle(&self, _: &App) -> FocusHandle {
3631                self.focus_handle.clone()
3632            }
3633        }
3634
3635        impl Render for NonBufferSearchableItem {
3636            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3637                div()
3638            }
3639        }
3640
3641        impl Item for NonBufferSearchableItem {
3642            type Event = ();
3643
3644            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
3645                "Non-buffer item".into()
3646            }
3647
3648            fn as_searchable(
3649                &self,
3650                handle: &Entity<Self>,
3651                _: &App,
3652            ) -> Option<Box<dyn SearchableItemHandle>> {
3653                Some(Box::new(handle.clone()))
3654            }
3655
3656            fn buffer_kind(&self, _: &App) -> ItemBufferKind {
3657                ItemBufferKind::None
3658            }
3659        }
3660
3661        impl SearchableItem for NonBufferSearchableItem {
3662            type Match = ();
3663
3664            fn clear_matches(&mut self, _: &mut Window, _: &mut Context<Self>) {}
3665            fn update_matches(
3666                &mut self,
3667                _: &[Self::Match],
3668                _: Option<usize>,
3669                _: SearchToken,
3670                _: &mut Window,
3671                _: &mut Context<Self>,
3672            ) {
3673            }
3674            fn query_suggestion(
3675                &mut self,
3676                _: Option<SeedQuerySetting>,
3677                _: &mut Window,
3678                _: &mut Context<Self>,
3679            ) -> String {
3680                String::new()
3681            }
3682            fn activate_match(
3683                &mut self,
3684                _: usize,
3685                _: &[Self::Match],
3686                _: SearchToken,
3687                _: &mut Window,
3688                _: &mut Context<Self>,
3689            ) {
3690            }
3691            fn select_matches(
3692                &mut self,
3693                _: &[Self::Match],
3694                _: SearchToken,
3695                _: &mut Window,
3696                _: &mut Context<Self>,
3697            ) {
3698            }
3699            fn replace(
3700                &mut self,
3701                _: &Self::Match,
3702                _: &SearchQuery,
3703                _: SearchToken,
3704                _: &mut Window,
3705                _: &mut Context<Self>,
3706            ) {
3707            }
3708            fn find_matches(
3709                &mut self,
3710                _: Arc<SearchQuery>,
3711                _: &mut Window,
3712                _: &mut Context<Self>,
3713            ) -> Task<Vec<Self::Match>> {
3714                Task::ready(Vec::new())
3715            }
3716            fn active_match_index(
3717                &mut self,
3718                _: Direction,
3719                _: &[Self::Match],
3720                _: SearchToken,
3721                _: &mut Window,
3722                _: &mut Context<Self>,
3723            ) -> Option<usize> {
3724                None
3725            }
3726        }
3727
3728        init_globals(cx);
3729        let window = cx.add_window(|window, cx| {
3730            let item = cx.new(|cx| NonBufferSearchableItem {
3731                focus_handle: cx.focus_handle(),
3732            });
3733            let mut search_bar = BufferSearchBar::new(None, window, cx);
3734            search_bar.set_active_pane_item(Some(&item), window, cx);
3735            search_bar
3736        });
3737        let search_bar = window.root(cx).unwrap();
3738        let cx = VisualTestContext::from_window(*window, cx).into_mut();
3739
3740        search_bar.read_with(cx, |search_bar, cx| {
3741            assert!(
3742                !search_bar.needs_expand_collapse_option(cx),
3743                "Items reporting ItemBufferKind::None must not get the expand/collapse button"
3744            );
3745        });
3746    }
3747
3748    #[perf]
3749    #[gpui::test]
3750    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
3751        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3752
3753        editor.update(cx, |editor, _| {
3754            editor.set_in_project_search(true);
3755        });
3756
3757        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
3758            search_bar.set_active_pane_item(Some(&editor), window, cx)
3759        });
3760
3761        assert_eq!(initial_location, ToolbarItemLocation::Hidden);
3762
3763        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);
3764
3765        search_bar.update_in(cx, |search_bar, window, cx| {
3766            search_bar.dismiss(&Dismiss, window, cx);
3767        });
3768
3769        assert_eq!(
3770            events.try_recv().unwrap(),
3771            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
3772        );
3773
3774        search_bar.update_in(cx, |search_bar, window, cx| {
3775            search_bar.show(window, cx);
3776        });
3777
3778        assert_eq!(
3779            events.try_recv().unwrap(),
3780            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
3781        );
3782    }
3783
3784    #[perf]
3785    #[gpui::test]
3786    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
3787        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3788
3789        search_bar.update_in(cx, |search_bar, window, cx| {
3790            search_bar.set_active_pane_item(Some(&editor), window, cx);
3791        });
3792
3793        editor.update_in(cx, |editor, window, cx| {
3794            editor.fold_all(&FoldAll, window, cx);
3795        });
3796        cx.run_until_parked();
3797
3798        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3799        assert!(is_collapsed);
3800
3801        editor.update_in(cx, |editor, window, cx| {
3802            editor.unfold_all(&UnfoldAll, window, cx);
3803        });
3804        cx.run_until_parked();
3805
3806        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3807        assert!(!is_collapsed);
3808    }
3809
3810    #[perf]
3811    #[gpui::test]
3812    async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
3813        let (editor, search_bar, cx) = init_multibuffer_test(cx);
3814
3815        search_bar.update_in(cx, |search_bar, window, cx| {
3816            search_bar.set_active_pane_item(Some(&editor), window, cx);
3817        });
3818
3819        // Fold all buffers via fold_all
3820        editor.update_in(cx, |editor, window, cx| {
3821            editor.fold_all(&FoldAll, window, cx);
3822        });
3823        cx.run_until_parked();
3824
3825        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3826        assert!(
3827            has_any_folded,
3828            "All buffers should be folded after fold_all"
3829        );
3830
3831        // Manually unfold one buffer (simulating a chevron click)
3832        let first_buffer_id = editor.read_with(cx, |editor, cx| {
3833            editor
3834                .buffer()
3835                .read(cx)
3836                .snapshot(cx)
3837                .excerpts()
3838                .nth(0)
3839                .unwrap()
3840                .context
3841                .start
3842                .buffer_id
3843        });
3844        editor.update_in(cx, |editor, _window, cx| {
3845            editor.unfold_buffer(first_buffer_id, cx);
3846        });
3847
3848        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3849        assert!(
3850            has_any_folded,
3851            "Should still report folds when only one buffer is unfolded"
3852        );
3853
3854        // Manually unfold the second buffer too
3855        let second_buffer_id = editor.read_with(cx, |editor, cx| {
3856            editor
3857                .buffer()
3858                .read(cx)
3859                .snapshot(cx)
3860                .excerpts()
3861                .nth(1)
3862                .unwrap()
3863                .context
3864                .start
3865                .buffer_id
3866        });
3867        editor.update_in(cx, |editor, _window, cx| {
3868            editor.unfold_buffer(second_buffer_id, cx);
3869        });
3870
3871        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3872        assert!(
3873            !has_any_folded,
3874            "No folds should remain after unfolding all buffers individually"
3875        );
3876
3877        // Manually fold one buffer back
3878        editor.update_in(cx, |editor, _window, cx| {
3879            editor.fold_buffer(first_buffer_id, cx);
3880        });
3881
3882        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
3883        assert!(
3884            has_any_folded,
3885            "Should report folds after manually folding one buffer"
3886        );
3887    }
3888
3889    #[perf]
3890    #[gpui::test]
3891    async fn test_search_options_changes(cx: &mut TestAppContext) {
3892        let (_editor, search_bar, cx) = init_test(cx);
3893        update_search_settings(
3894            SearchSettings {
3895                button: true,
3896                whole_word: false,
3897                case_sensitive: false,
3898                include_ignored: false,
3899                regex: false,
3900                center_on_match: false,
3901            },
3902            cx,
3903        );
3904
3905        let deploy = Deploy {
3906            focus: true,
3907            replace_enabled: false,
3908            selection_search_enabled: true,
3909        };
3910
3911        search_bar.update_in(cx, |search_bar, window, cx| {
3912            assert_eq!(
3913                search_bar.search_options,
3914                SearchOptions::NONE,
3915                "Should have no search options enabled by default"
3916            );
3917            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3918            assert_eq!(
3919                search_bar.search_options,
3920                SearchOptions::WHOLE_WORD,
3921                "Should enable the option toggled"
3922            );
3923            assert!(
3924                !search_bar.dismissed,
3925                "Search bar should be present and visible"
3926            );
3927            search_bar.deploy(&deploy, None, window, cx);
3928            assert_eq!(
3929                search_bar.search_options,
3930                SearchOptions::WHOLE_WORD,
3931                "After (re)deploying, the option should still be enabled"
3932            );
3933
3934            search_bar.dismiss(&Dismiss, window, cx);
3935            search_bar.deploy(&deploy, None, window, cx);
3936            assert_eq!(
3937                search_bar.search_options,
3938                SearchOptions::WHOLE_WORD,
3939                "After hiding and showing the search bar, search options should be preserved"
3940            );
3941
3942            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
3943            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3944            assert_eq!(
3945                search_bar.search_options,
3946                SearchOptions::REGEX,
3947                "Should enable the options toggled"
3948            );
3949            assert!(
3950                !search_bar.dismissed,
3951                "Search bar should be present and visible"
3952            );
3953            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
3954        });
3955
3956        update_search_settings(
3957            SearchSettings {
3958                button: true,
3959                whole_word: false,
3960                case_sensitive: true,
3961                include_ignored: false,
3962                regex: false,
3963                center_on_match: false,
3964            },
3965            cx,
3966        );
3967        search_bar.update_in(cx, |search_bar, window, cx| {
3968            assert_eq!(
3969                search_bar.search_options,
3970                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3971                "Should have no search options enabled by default"
3972            );
3973
3974            search_bar.deploy(&deploy, None, window, cx);
3975            assert_eq!(
3976                search_bar.search_options,
3977                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
3978                "Toggling a non-dismissed search bar with custom options should not change the default options"
3979            );
3980            search_bar.dismiss(&Dismiss, window, cx);
3981            search_bar.deploy(&deploy, None, window, cx);
3982            assert_eq!(
3983                search_bar.configured_options,
3984                SearchOptions::CASE_SENSITIVE,
3985                "After a settings update and toggling the search bar, configured options should be updated"
3986            );
3987            assert_eq!(
3988                search_bar.search_options,
3989                SearchOptions::CASE_SENSITIVE,
3990                "After a settings update and toggling the search bar, configured options should be used"
3991            );
3992        });
3993
3994        update_search_settings(
3995            SearchSettings {
3996                button: true,
3997                whole_word: true,
3998                case_sensitive: true,
3999                include_ignored: false,
4000                regex: false,
4001                center_on_match: false,
4002            },
4003            cx,
4004        );
4005
4006        search_bar.update_in(cx, |search_bar, window, cx| {
4007            search_bar.deploy(&deploy, None, window, cx);
4008            search_bar.dismiss(&Dismiss, window, cx);
4009            search_bar.show(window, cx);
4010            assert_eq!(
4011                search_bar.search_options,
4012                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
4013                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
4014            );
4015        });
4016    }
4017
4018    #[gpui::test]
4019    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
4020        let (editor, search_bar, cx) = init_test(cx);
4021        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
4022
4023        // Start with case sensitive search settings.
4024        let mut search_settings = SearchSettings::default();
4025        search_settings.case_sensitive = true;
4026        update_search_settings(search_settings, cx);
4027        search_bar.update(cx, |search_bar, cx| {
4028            let mut search_options = search_bar.search_options;
4029            search_options.insert(SearchOptions::CASE_SENSITIVE);
4030            search_bar.set_search_options(search_options, cx);
4031        });
4032
4033        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
4034        editor_cx.update_editor(|e, window, cx| {
4035            e.select_next(&Default::default(), window, cx).unwrap();
4036        });
4037        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
4038
4039        // Update the search bar's case sensitivite toggle, so we can later
4040        // confirm that `select_next` will now be case-insensitive.
4041        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
4042        search_bar.update_in(cx, |search_bar, window, cx| {
4043            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
4044        });
4045        editor_cx.update_editor(|e, window, cx| {
4046            e.select_next(&Default::default(), window, cx).unwrap();
4047        });
4048        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
4049
4050        // Confirm that, after dismissing the search bar, only the editor's
4051        // search settings actually affect the behavior of `select_next`.
4052        search_bar.update_in(cx, |search_bar, window, cx| {
4053            search_bar.dismiss(&Default::default(), window, cx);
4054        });
4055        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
4056        editor_cx.update_editor(|e, window, cx| {
4057            e.select_next(&Default::default(), window, cx).unwrap();
4058        });
4059        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
4060
4061        // Update the editor's search settings, disabling case sensitivity, to
4062        // check that the value is respected.
4063        let mut search_settings = SearchSettings::default();
4064        search_settings.case_sensitive = false;
4065        update_search_settings(search_settings, cx);
4066        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
4067        editor_cx.update_editor(|e, window, cx| {
4068            e.select_next(&Default::default(), window, cx).unwrap();
4069        });
4070        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
4071    }
4072
4073    #[gpui::test]
4074    async fn test_regex_search_does_not_highlight_non_matching_occurrences(
4075        cx: &mut TestAppContext,
4076    ) {
4077        init_globals(cx);
4078        let buffer = cx.new(|cx| {
4079            Buffer::local(
4080                "something is at the top\nsomething is behind something\nsomething is at the bottom\n",
4081                cx,
4082            )
4083        });
4084        let cx = cx.add_empty_window();
4085        let editor =
4086            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
4087        let search_bar = cx.new_window_entity(|window, cx| {
4088            let mut search_bar = BufferSearchBar::new(None, window, cx);
4089            search_bar.set_active_pane_item(Some(&editor), window, cx);
4090            search_bar.show(window, cx);
4091            search_bar
4092        });
4093
4094        search_bar.update_in(cx, |search_bar, window, cx| {
4095            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
4096        });
4097
4098        search_bar
4099            .update_in(cx, |search_bar, window, cx| {
4100                search_bar.search("^something", None, true, window, cx)
4101            })
4102            .await
4103            .unwrap();
4104
4105        search_bar.update_in(cx, |search_bar, window, cx| {
4106            search_bar.select_next_match(&SelectNextMatch, window, cx);
4107        });
4108
4109        // Advance past the debounce so the selection occurrence highlight would
4110        // have fired if it were not suppressed by the active buffer search.
4111        cx.executor()
4112            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
4113        cx.run_until_parked();
4114
4115        editor.update(cx, |editor, cx| {
4116            assert!(
4117                !editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
4118                "selection occurrence highlights must be suppressed during buffer search"
4119            );
4120            assert_eq!(
4121                editor.search_background_highlights(cx).len(),
4122                3,
4123                "expected exactly 3 search highlights (one per line start)"
4124            );
4125        });
4126
4127        // Manually select "something" — this should restore occurrence highlights
4128        // because it clears the search-navigation flag.
4129        editor.update_in(cx, |editor, window, cx| {
4130            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4131                s.select_ranges([Point::new(0, 0)..Point::new(0, 9)])
4132            });
4133        });
4134
4135        cx.executor()
4136            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
4137        cx.run_until_parked();
4138
4139        editor.update(cx, |editor, _cx| {
4140            assert!(
4141                editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
4142                "selection occurrence highlights must be restored after a manual selection"
4143            );
4144        });
4145    }
4146
4147    #[gpui::test]
4148    async fn test_replace_with_non_ascii_characters(cx: &mut TestAppContext) {
4149        let (editor, search_bar, cx) = init_test(cx);
4150
4151        editor.update_in(cx, |editor, window, cx| {
4152            editor.set_text("¥100 ¥200 ¥100", window, cx)
4153        });
4154
4155        search_bar
4156            .update_in(cx, |search_bar, window, cx| {
4157                search_bar.search("¥", None, true, window, cx)
4158            })
4159            .await
4160            .unwrap();
4161
4162        search_bar.update_in(cx, |search_bar, window, cx| {
4163            search_bar.replacement_editor.update(cx, |editor, cx| {
4164                editor.set_text("\\n", window, cx);
4165            });
4166            search_bar.replace_all(&ReplaceAll, window, cx)
4167        });
4168
4169        assert_eq!(
4170            editor.read_with(cx, |this, cx| this.text(cx)),
4171            "\\n100 \\n200 \\n100"
4172        );
4173    }
4174
4175    #[gpui::test]
4176    async fn test_seeded_query_is_escaped_in_regex_mode(cx: &mut TestAppContext) {
4177        init_globals(cx);
4178        let buffer = cx.new(|cx| Buffer::local("z.d\nzed\n", cx));
4179        let cx = cx.add_empty_window();
4180        let editor =
4181            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
4182        let search_bar = cx.new_window_entity(|window, cx| {
4183            let mut search_bar = BufferSearchBar::new(None, window, cx);
4184            search_bar.set_active_pane_item(Some(&editor), window, cx);
4185            search_bar.show(window, cx);
4186            search_bar
4187        });
4188
4189        // Select "z.d" on the first line.
4190        editor.update_in(cx, |editor, window, cx| {
4191            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4192                s.select_ranges([Point::new(0, 0)..Point::new(0, 3)])
4193            });
4194        });
4195
4196        search_bar.update_in(cx, |search_bar, window, cx| {
4197            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
4198            search_bar.search_suggested(Some(SeedQuerySetting::Selection), window, cx);
4199        });
4200        cx.run_until_parked();
4201
4202        search_bar.read_with(cx, |search_bar, cx| {
4203            assert_eq!(
4204                search_bar.query(cx),
4205                r"z\.d",
4206                "seeded regex query must be escaped"
4207            );
4208        });
4209
4210        editor.update(cx, |editor, cx| {
4211            assert_eq!(
4212                editor.search_background_highlights(cx).len(),
4213                1,
4214                "only the literal 'z.d' should match, not 'zed'"
4215            );
4216        });
4217    }
4218
4219    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
4220        cx.update(|cx| {
4221            SettingsStore::update_global(cx, |store, cx| {
4222                store.update_user_settings(cx, |settings| {
4223                    settings.editor.search = Some(SearchSettingsContent {
4224                        button: Some(search_settings.button),
4225                        whole_word: Some(search_settings.whole_word),
4226                        case_sensitive: Some(search_settings.case_sensitive),
4227                        include_ignored: Some(search_settings.include_ignored),
4228                        regex: Some(search_settings.regex),
4229                        center_on_match: Some(search_settings.center_on_match),
4230                    });
4231                });
4232            });
4233        });
4234    }
4235}
4236
Served at tenant.openagents/omega Member data and write actions are omitted.