mod registrar;

use crate::{
    FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
    SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
    ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
    buffer_search::registrar::WithResultsOrExternalQuery,
    search_bar::{
        ActionButtonState, HistoryNavigationDirection, alignment_element,
        filter_search_results_input, input_base_styles, render_action_button, render_text_input,
        should_navigate_history,
    },
};
use any_vec::AnyVec;
use collections::HashMap;
use editor::{
    Editor, EditorSettings, MultiBufferOffset, SplittableEditor, ToggleSplitDiff,
    actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
    scroll::Autoscroll,
};
use futures::channel::oneshot;
use gpui::{
    Action as _, App, ClickEvent, Context, Entity, EventEmitter, Focusable,
    InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
    Styled, Subscription, Task, TaskExt, WeakEntity, Window, div,
};
use language::{Language, LanguageRegistry};
use project::{
    search::SearchQuery,
    search_history::{SearchHistory, SearchHistoryCursor},
};

use fs::Fs;
use settings::{DiffViewStyle, SeedQuerySetting, Settings, update_settings_file};
use std::{any::TypeId, sync::Arc};
use zed_actions::{
    OpenSettingsAt, outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath,
};

use ui::{
    BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,
    render_modifiers, utils::SearchInputWidth,
};
use util::{ResultExt, paths::PathMatcher};
use workspace::{
    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
    item::{ItemBufferKind, ItemHandle},
    searchable::{
        Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItemHandle,
        WeakSearchableItemHandle,
    },
};

pub use registrar::{DivRegistrar, register_pane_search_actions};
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar};

const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;

pub use zed_actions::buffer_search::{
    Deploy, DeployReplace, Dismiss, FocusEditor, UseSelectionForFind,
};

pub enum Event {
    UpdateLocation,
    Dismissed,
}

pub fn init(cx: &mut App) {
    cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
        .detach();
}

pub struct BufferSearchBar {
    query_editor: Entity<Editor>,
    query_editor_focused: bool,
    replacement_editor: Entity<Editor>,
    replacement_editor_focused: bool,
    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
    active_match_index: Option<usize>,
    #[cfg(target_os = "macos")]
    active_searchable_item_subscriptions: Option<[Subscription; 2]>,
    #[cfg(not(target_os = "macos"))]
    active_searchable_item_subscriptions: Option<Subscription>,
    #[cfg(target_os = "macos")]
    pending_external_query: Option<(String, SearchOptions)>,
    active_search: Option<Arc<SearchQuery>>,
    searchable_items_with_matches:
        HashMap<Box<dyn WeakSearchableItemHandle>, (AnyVec<dyn Send>, SearchToken)>,
    pending_search: Option<Task<()>>,
    search_options: SearchOptions,
    default_options: SearchOptions,
    configured_options: SearchOptions,
    query_error: Option<String>,
    dismissed: bool,
    search_history: SearchHistory,
    search_history_cursor: SearchHistoryCursor,
    replace_enabled: bool,
    selection_search_enabled: Option<FilteredSearchRange>,
    scroll_handle: ScrollHandle,
    regex_language: Option<Arc<Language>>,
    splittable_editor: Option<WeakEntity<SplittableEditor>>,
    _splittable_editor_subscription: Option<Subscription>,
}

impl EventEmitter<Event> for BufferSearchBar {}
impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
impl Render for BufferSearchBar {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let focus_handle = self.focus_handle(cx);

        let has_splittable_editor = self.splittable_editor.is_some();
        let split_buttons = if has_splittable_editor {
            self.splittable_editor
                .as_ref()
                .and_then(|weak| weak.upgrade())
                .map(|splittable_editor| {
                    let editor_ref = splittable_editor.read(cx);
                    let diff_view_style = editor_ref.diff_view_style();

                    let is_split_set = diff_view_style == DiffViewStyle::Split;
                    let is_split_active = editor_ref.is_split();
                    let min_columns =
                        EditorSettings::get_global(cx).minimum_split_diff_width as u32;

                    let split_icon = if is_split_set && !is_split_active {
                        IconName::DiffSplitAuto
                    } else {
                        IconName::DiffSplit
                    };

                    h_flex()
                        .gap_1()
                        .child(
                            IconButton::new("diff-unified", IconName::DiffUnified)
                                .icon_size(IconSize::Small)
                                .toggle_state(diff_view_style == DiffViewStyle::Unified)
                                .tooltip(Tooltip::text("Unified"))
                                .on_click({
                                    let splittable_editor = splittable_editor.downgrade();
                                    move |_, window, cx| {
                                        update_settings_file(
                                            <dyn Fs>::global(cx),
                                            cx,
                                            |settings, _| {
                                                settings.editor.diff_view_style =
                                                    Some(DiffViewStyle::Unified);
                                            },
                                        );
                                        if diff_view_style == DiffViewStyle::Split {
                                            splittable_editor
                                                .update(cx, |editor, cx| {
                                                    editor.toggle_split(
                                                        &ToggleSplitDiff,
                                                        window,
                                                        cx,
                                                    );
                                                })
                                                .ok();
                                        }
                                    }
                                }),
                        )
                        .child(
                            IconButton::new("diff-split", split_icon)
                                .toggle_state(diff_view_style == DiffViewStyle::Split)
                                .icon_size(IconSize::Small)
                                .tooltip(Tooltip::element(move |_, cx| {
                                    let message = if is_split_set && !is_split_active {
                                        format!("Split when wider than {} columns", min_columns)
                                            .into()
                                    } else {
                                        SharedString::from("Split")
                                    };

                                    v_flex()
                                        .child(message)
                                        .child(
                                            h_flex()
                                                .gap_0p5()
                                                .text_ui_sm(cx)
                                                .text_color(Color::Muted.color(cx))
                                                .children(render_modifiers(
                                                    &gpui::Modifiers::secondary_key(),
                                                    PlatformStyle::platform(),
                                                    None,
                                                    Some(TextSize::Small.rems(cx).into()),
                                                    false,
                                                ))
                                                .child("click to change min width"),
                                        )
                                        .into_any()
                                }))
                                .on_click({
                                    let splittable_editor = splittable_editor.downgrade();
                                    move |_, window, cx| {
                                        if window.modifiers().secondary() {
                                            window.dispatch_action(
                                                OpenSettingsAt {
                                                    path: "minimum_split_diff_width".to_string(),
                                                }
                                                .boxed_clone(),
                                                cx,
                                            );
                                        } else {
                                            update_settings_file(
                                                <dyn Fs>::global(cx),
                                                cx,
                                                |settings, _| {
                                                    settings.editor.diff_view_style =
                                                        Some(DiffViewStyle::Split);
                                                },
                                            );
                                            if diff_view_style == DiffViewStyle::Unified {
                                                splittable_editor
                                                    .update(cx, |editor, cx| {
                                                        editor.toggle_split(
                                                            &ToggleSplitDiff,
                                                            window,
                                                            cx,
                                                        );
                                                    })
                                                    .ok();
                                            }
                                        }
                                    }
                                }),
                        )
                })
        } else {
            None
        };

        let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
            let query_editor_focus = self.query_editor.focus_handle(cx);

            let is_collapsed = self
                .active_searchable_item
                .as_ref()
                .and_then(|item| item.act_as_type(TypeId::of::<Editor>(), cx))
                .and_then(|item| item.downcast::<Editor>().ok())
                .map(|editor: Entity<Editor>| editor.read(cx).has_any_buffer_folded(cx))
                .unwrap_or_default();
            let (icon, tooltip_label) = if is_collapsed {
                (IconName::ChevronUpDown, "Expand All Files")
            } else {
                (IconName::ChevronDownUp, "Collapse All Files")
            };

            let collapse_expand_icon_button = |id| {
                IconButton::new(id, icon)
                    .icon_size(IconSize::Small)
                    .tooltip(move |_, cx| {
                        Tooltip::for_action_in(
                            tooltip_label,
                            &ToggleFoldAll,
                            &query_editor_focus,
                            cx,
                        )
                    })
                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
                        this.toggle_fold_all(&ToggleFoldAll, window, cx);
                    }))
            };

            if self.dismissed {
                return h_flex()
                    .pl_0p5()
                    .gap_1()
                    .child(collapse_expand_icon_button(
                        "multibuffer-collapse-expand-empty",
                    ))
                    .when(has_splittable_editor, |this| this.children(split_buttons))
                    .into_any_element();
            }

            Some(
                h_flex()
                    .gap_1()
                    .child(collapse_expand_icon_button("multibuffer-collapse-expand"))
                    .children(split_buttons)
                    .into_any_element(),
            )
        } else {
            None
        };

        let narrow_mode =
            self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;

        let workspace::searchable::SearchOptions {
            case,
            word,
            regex,
            replacement,
            selection,
            select_all,
            find_in_results,
        } = self.supported_options(cx);

        self.query_editor.update(cx, |query_editor, cx| {
            if query_editor.placeholder_text(cx).is_none() {
                query_editor.set_placeholder_text("Search…", window, cx);
            }
        });

        self.replacement_editor.update(cx, |editor, cx| {
            editor.set_placeholder_text("Replace with…", window, cx);
        });

        let mut color_override = None;
        let match_text = self
            .active_searchable_item
            .as_ref()
            .and_then(|searchable_item| {
                if self.query(cx).is_empty() {
                    return None;
                }
                let matches_count = self
                    .searchable_items_with_matches
                    .get(&searchable_item.downgrade())
                    .map(|(matches, _)| matches.len())
                    .unwrap_or(0);
                if let Some(match_ix) = self.active_match_index {
                    Some(format!("{}/{}", match_ix + 1, matches_count))
                } else {
                    color_override = Some(Color::Error); // No matches found
                    None
                }
            })
            .unwrap_or_else(|| "0/0".to_string());
        let should_show_replace_input = self.replace_enabled && replacement;
        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);

        let theme_colors = cx.theme().colors();
        let query_border = if self.query_error.is_some() {
            Color::Error.color(cx)
        } else {
            theme_colors.border
        };
        let replacement_border = theme_colors.border;

        let container_width = window.viewport_size().width;
        let input_width = SearchInputWidth::calc_width(container_width);

        let input_base_styles =
            |border_color| input_base_styles(border_color, |div| div.w(input_width));

        let input_style = if find_in_results {
            filter_search_results_input(query_border, |div| div.w(input_width), cx)
        } else {
            input_base_styles(query_border)
        };

        let query_column = input_style
            .child(div().flex_1().min_w_0().py_1().child(render_text_input(
                &self.query_editor,
                color_override,
                cx,
            )))
            .child(
                h_flex()
                    .flex_none()
                    .gap_1()
                    .when(case, |div| {
                        div.child(SearchOption::CaseSensitive.as_button(
                            self.search_options,
                            SearchSource::Buffer,
                            focus_handle.clone(),
                        ))
                    })
                    .when(word, |div| {
                        div.child(SearchOption::WholeWord.as_button(
                            self.search_options,
                            SearchSource::Buffer,
                            focus_handle.clone(),
                        ))
                    })
                    .when(regex, |div| {
                        div.child(SearchOption::Regex.as_button(
                            self.search_options,
                            SearchSource::Buffer,
                            focus_handle.clone(),
                        ))
                    }),
            );

        let mode_column = h_flex()
            .gap_1()
            .min_w_64()
            .when(replacement, |this| {
                this.child(render_action_button(
                    "buffer-search-bar-toggle",
                    IconName::Replace,
                    self.replace_enabled.then_some(ActionButtonState::Toggled),
                    "Toggle Replace",
                    &ToggleReplace,
                    focus_handle.clone(),
                ))
            })
            .when(selection, |this| {
                this.child(
                    IconButton::new(
                        "buffer-search-bar-toggle-search-selection-button",
                        IconName::Quote,
                    )
                    .style(ButtonStyle::Subtle)
                    .shape(IconButtonShape::Square)
                    .when(self.selection_search_enabled.is_some(), |button| {
                        button.style(ButtonStyle::Filled)
                    })
                    .on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
                        this.toggle_selection(&ToggleSelection, window, cx);
                    }))
                    .toggle_state(self.selection_search_enabled.is_some())
                    .tooltip({
                        let focus_handle = focus_handle.clone();
                        move |_window, cx| {
                            Tooltip::for_action_in(
                                "Toggle Search Selection",
                                &ToggleSelection,
                                &focus_handle,
                                cx,
                            )
                        }
                    }),
                )
            })
            .when(!find_in_results, |el| {
                let query_focus = self.query_editor.focus_handle(cx);
                let matches_column = h_flex()
                    .pl_2()
                    .ml_2()
                    .border_l_1()
                    .border_color(theme_colors.border_variant)
                    .child(render_action_button(
                        "buffer-search-nav-button",
                        ui::IconName::ChevronLeft,
                        self.active_match_index
                            .is_none()
                            .then_some(ActionButtonState::Disabled),
                        "Select Previous Match",
                        &SelectPreviousMatch,
                        query_focus.clone(),
                    ))
                    .child(render_action_button(
                        "buffer-search-nav-button",
                        ui::IconName::ChevronRight,
                        self.active_match_index
                            .is_none()
                            .then_some(ActionButtonState::Disabled),
                        "Select Next Match",
                        &SelectNextMatch,
                        query_focus.clone(),
                    ))
                    .when(!narrow_mode, |this| {
                        this.child(div().ml_2().min_w(rems_from_px(40.)).child(
                            Label::new(match_text).size(LabelSize::Small).color(
                                if self.active_match_index.is_some() {
                                    Color::Default
                                } else {
                                    Color::Disabled
                                },
                            ),
                        ))
                    });

                el.when(select_all, |el| {
                    el.child(render_action_button(
                        "buffer-search-nav-button",
                        IconName::SelectAll,
                        Default::default(),
                        "Select All Matches",
                        &SelectAllMatches,
                        query_focus.clone(),
                    ))
                })
                .child(matches_column)
            })
            .when(find_in_results, |el| {
                el.child(render_action_button(
                    "buffer-search",
                    IconName::Close,
                    Default::default(),
                    "Close Search Bar",
                    &Dismiss,
                    focus_handle.clone(),
                ))
            });

        let has_collapse_button = collapse_expand_button.is_some();

        let search_line = h_flex()
            .w_full()
            .gap_2()
            .when(find_in_results, |el| el.child(alignment_element()))
            .when(!find_in_results && has_collapse_button, |el| {
                el.pl_0p5().child(collapse_expand_button.expect("button"))
            })
            .child(query_column)
            .child(mode_column);

        let replace_line = should_show_replace_input.then(|| {
            let replace_column = input_base_styles(replacement_border).child(
                div()
                    .flex_1()
                    .py_1()
                    .child(render_text_input(&self.replacement_editor, None, cx)),
            );
            let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);

            let replace_actions = h_flex()
                .min_w_64()
                .gap_1()
                .child(render_action_button(
                    "buffer-search-replace-button",
                    IconName::ReplaceNext,
                    Default::default(),
                    "Replace Next Match",
                    &ReplaceNext,
                    focus_handle.clone(),
                ))
                .child(render_action_button(
                    "buffer-search-replace-button",
                    IconName::ReplaceAll,
                    Default::default(),
                    "Replace All Matches",
                    &ReplaceAll,
                    focus_handle,
                ));

            h_flex()
                .w_full()
                .gap_2()
                .when(has_collapse_button, |this| this.child(alignment_element()))
                .child(replace_column)
                .child(replace_actions)
        });

        let mut key_context = KeyContext::new_with_defaults();
        key_context.add("BufferSearchBar");
        if in_replace {
            key_context.add("in_replace");
        }

        let query_error_line = self.query_error.as_ref().map(|error| {
            Label::new(error)
                .size(LabelSize::Small)
                .color(Color::Error)
                .mt_neg_1()
                .ml_2()
        });

        let search_line =
            h_flex()
                .relative()
                .child(search_line)
                .when(!narrow_mode && !find_in_results, |this| {
                    this.child(
                        h_flex()
                            .absolute()
                            .right_0()
                            .when(has_collapse_button, |this| {
                                this.pr_2()
                                    .border_r_1()
                                    .border_color(cx.theme().colors().border_variant)
                            })
                            .child(render_action_button(
                                "buffer-search",
                                IconName::Close,
                                Default::default(),
                                "Close Search Bar",
                                &Dismiss,
                                focus_handle.clone(),
                            )),
                    )
                });

        v_flex()
            .id("buffer_search")
            .gap_2()
            .w_full()
            .track_scroll(&self.scroll_handle)
            .key_context(key_context)
            .capture_action(cx.listener(Self::tab))
            .capture_action(cx.listener(Self::backtab))
            .capture_action(cx.listener(Self::toggle_fold_all))
            .on_action(cx.listener(Self::previous_history_query))
            .on_action(cx.listener(Self::next_history_query))
            .on_action(cx.listener(Self::dismiss))
            .on_action(cx.listener(Self::select_next_match))
            .on_action(cx.listener(Self::select_prev_match))
            .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
                if let Some(active_searchable_item) = &mut this.active_searchable_item {
                    active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
                }
            }))
            .on_action(cx.listener(|this, _: &CopyPath, window, cx| {
                if let Some(active_searchable_item) = &mut this.active_searchable_item {
                    active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
                }
            }))
            .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
                if let Some(active_searchable_item) = &mut this.active_searchable_item {
                    active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
                }
            }))
            .when(replacement, |this| {
                this.on_action(cx.listener(Self::toggle_replace))
                    .on_action(cx.listener(Self::replace_next))
                    .on_action(cx.listener(Self::replace_all))
            })
            .when(case, |this| {
                this.on_action(cx.listener(Self::toggle_case_sensitive))
            })
            .when(word, |this| {
                this.on_action(cx.listener(Self::toggle_whole_word))
            })
            .when(regex, |this| {
                this.on_action(cx.listener(Self::toggle_regex))
            })
            .when(selection, |this| {
                this.on_action(cx.listener(Self::toggle_selection))
            })
            .child(search_line)
            .children(query_error_line)
            .children(replace_line)
            .into_any_element()
    }
}

impl Focusable for BufferSearchBar {
    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
        self.query_editor.focus_handle(cx)
    }
}

impl ToolbarItemView for BufferSearchBar {
    fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
        if !self.dismissed {
            context.add("buffer_search_deployed");
        }
    }

    fn set_active_pane_item(
        &mut self,
        item: Option<&dyn ItemHandle>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> ToolbarItemLocation {
        cx.notify();
        self.active_searchable_item_subscriptions.take();
        self.active_searchable_item.take();
        self.splittable_editor = None;
        self._splittable_editor_subscription = None;

        self.pending_search.take();

        if let Some(splittable_editor) = item
            .and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
            .and_then(|entity| entity.downcast::<SplittableEditor>().ok())
        {
            self._splittable_editor_subscription =
                Some(cx.observe(&splittable_editor, |_, _, cx| cx.notify()));
            self.splittable_editor = Some(splittable_editor.downgrade());
        }

        if let Some(searchable_item_handle) =
            item.and_then(|item| item.to_searchable_item_handle(cx))
        {
            let this = cx.entity().downgrade();

            let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
                window,
                cx,
                Box::new(move |search_event, window, cx| {
                    if let Some(this) = this.upgrade() {
                        this.update(cx, |this, cx| {
                            this.on_active_searchable_item_event(search_event, window, cx)
                        });
                    }
                }),
            );

            #[cfg(target_os = "macos")]
            {
                let item_focus_handle = searchable_item_handle.item_focus_handle(cx);

                self.active_searchable_item_subscriptions = Some([
                    search_event_subscription,
                    cx.on_focus(&item_focus_handle, window, |this, window, cx| {
                        if this.query_editor_focused || this.replacement_editor_focused {
                            // no need to read pasteboard since focus came from toolbar
                            return;
                        }

                        cx.defer_in(window, |this, window, cx| {
                            let Some(item) = cx.read_from_find_pasteboard() else {
                                return;
                            };
                            let Some(text) = item.text() else {
                                return;
                            };

                            if this.query(cx) == text {
                                return;
                            }

                            let search_options = item
                                .metadata()
                                .and_then(|m| m.parse().ok())
                                .and_then(SearchOptions::from_bits)
                                .unwrap_or(this.search_options);

                            if this.dismissed {
                                this.pending_external_query = Some((text, search_options));
                            } else {
                                drop(this.search(&text, Some(search_options), true, window, cx));
                            }
                        });
                    }),
                ]);
            }
            #[cfg(not(target_os = "macos"))]
            {
                self.active_searchable_item_subscriptions = Some(search_event_subscription);
            }

            let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
            self.active_searchable_item = Some(searchable_item_handle);
            drop(self.update_matches(true, false, window, cx));
            if self.needs_expand_collapse_option(cx) {
                return ToolbarItemLocation::PrimaryLeft;
            } else if !self.is_dismissed() {
                if is_project_search {
                    self.dismiss(&Default::default(), window, cx);
                } else {
                    return ToolbarItemLocation::Secondary;
                }
            }
        }
        ToolbarItemLocation::Hidden
    }
}

impl BufferSearchBar {
    pub fn query_editor_focused(&self) -> bool {
        self.query_editor_focused
    }

    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
        registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
            this.query_editor.focus_handle(cx).focus(window, cx);
            this.select_query(window, cx);
        }));
        registrar.register_handler(ForDeployed(
            |this, action: &ToggleCaseSensitive, window, cx| {
                if this.supported_options(cx).case {
                    this.toggle_case_sensitive(action, window, cx);
                }
            },
        ));
        registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
            if this.supported_options(cx).word {
                this.toggle_whole_word(action, window, cx);
            }
        }));
        registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
            if this.supported_options(cx).regex {
                this.toggle_regex(action, window, cx);
            }
        }));
        registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
            if this.supported_options(cx).selection {
                this.toggle_selection(action, window, cx);
            } else {
                cx.propagate();
            }
        }));
        registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
            if this.supported_options(cx).replacement {
                this.toggle_replace(action, window, cx);
            } else {
                cx.propagate();
            }
        }));
        registrar.register_handler(WithResultsOrExternalQuery(
            |this, action: &SelectNextMatch, window, cx| {
                if this.supported_options(cx).find_in_results {
                    cx.propagate();
                } else {
                    this.select_next_match(action, window, cx);
                }
            },
        ));
        registrar.register_handler(WithResultsOrExternalQuery(
            |this, action: &SelectPreviousMatch, window, cx| {
                if this.supported_options(cx).find_in_results {
                    cx.propagate();
                } else {
                    this.select_prev_match(action, window, cx);
                }
            },
        ));
        registrar.register_handler(WithResultsOrExternalQuery(
            |this, action: &SelectAllMatches, window, cx| {
                if this.supported_options(cx).find_in_results {
                    cx.propagate();
                } else {
                    this.select_all_matches(action, window, cx);
                }
            },
        ));
        registrar.register_handler(ForDeployed(
            |this, _: &editor::actions::Cancel, window, cx| {
                this.dismiss(&Dismiss, window, cx);
            },
        ));
        registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
            this.dismiss(&Dismiss, window, cx);
        }));

        // register deploy buffer search for both search bar states, since we want to focus into the search bar
        // when the deploy action is triggered in the buffer.
        registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
            this.deploy(deploy, None, window, cx);
        }));
        registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
            this.deploy(deploy, None, window, cx);
        }));
        registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
            if this.supported_options(cx).find_in_results {
                cx.propagate();
            } else {
                this.deploy(&Deploy::replace(), None, window, cx);
            }
        }));
        registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
            if this.supported_options(cx).find_in_results {
                cx.propagate();
            } else {
                this.deploy(&Deploy::replace(), None, window, cx);
            }
        }));
        registrar.register_handler(ForDeployed(
            |this, action: &UseSelectionForFind, window, cx| {
                this.use_selection_for_find(action, window, cx);
            },
        ));
        registrar.register_handler(ForDismissed(
            |this, action: &UseSelectionForFind, window, cx| {
                this.use_selection_for_find(action, window, cx);
            },
        ));
    }

    pub fn new(
        languages: Option<Arc<LanguageRegistry>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let query_editor = cx.new(|cx| {
            let mut editor = Editor::auto_height(1, 4, window, cx);
            editor.set_use_autoclose(false);
            editor.set_use_selection_highlight(false);
            editor
        });
        cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
            .detach();
        let replacement_editor = cx.new(|cx| Editor::auto_height(1, 4, window, cx));
        cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
            .detach();

        let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
        if let Some(languages) = languages {
            let query_buffer = query_editor
                .read(cx)
                .buffer()
                .read(cx)
                .as_singleton()
                .expect("query editor should be backed by a singleton buffer");

            query_buffer
                .read(cx)
                .set_language_registry(languages.clone());

            cx.spawn(async move |buffer_search_bar, cx| {
                use anyhow::Context as _;

                let regex_language = languages
                    .language_for_name("regex")
                    .await
                    .context("loading regex language")?;

                buffer_search_bar
                    .update(cx, |buffer_search_bar, cx| {
                        buffer_search_bar.regex_language = Some(regex_language);
                        buffer_search_bar.adjust_query_regex_language(cx);
                    })
                    .ok();
                anyhow::Ok(())
            })
            .detach_and_log_err(cx);
        }

        Self {
            query_editor,
            query_editor_focused: false,
            replacement_editor,
            replacement_editor_focused: false,
            active_searchable_item: None,
            active_searchable_item_subscriptions: None,
            #[cfg(target_os = "macos")]
            pending_external_query: None,
            active_match_index: None,
            searchable_items_with_matches: Default::default(),
            default_options: search_options,
            configured_options: search_options,
            search_options,
            pending_search: None,
            query_error: None,
            dismissed: true,
            search_history: SearchHistory::new(
                Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
                project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
            ),
            search_history_cursor: Default::default(),
            active_search: None,
            replace_enabled: false,
            selection_search_enabled: None,
            scroll_handle: ScrollHandle::new(),
            regex_language: None,
            splittable_editor: None,
            _splittable_editor_subscription: None,
        }
    }

    pub fn is_dismissed(&self) -> bool {
        self.dismissed
    }

    pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
        self.dismissed = true;
        cx.emit(Event::Dismissed);
        self.query_error = None;
        self.sync_select_next_case_sensitivity(cx);

        for searchable_item in self.searchable_items_with_matches.keys() {
            if let Some(searchable_item) =
                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
            {
                searchable_item.clear_matches(window, cx);
            }
        }

        let needs_collapse_expand = self.needs_expand_collapse_option(cx);

        if let Some(active_editor) = self.active_searchable_item.as_mut() {
            self.selection_search_enabled = None;
            self.replace_enabled = false;
            active_editor.search_bar_visibility_changed(false, window, cx);
            active_editor.toggle_filtered_search_ranges(None, window, cx);
            let handle = active_editor.item_focus_handle(cx);
            self.focus(&handle, window, cx);
        }

        if needs_collapse_expand {
            cx.emit(Event::UpdateLocation);
            cx.emit(ToolbarItemEvent::ChangeLocation(
                ToolbarItemLocation::PrimaryLeft,
            ));
            cx.notify();
            return;
        }
        cx.emit(Event::UpdateLocation);
        cx.emit(ToolbarItemEvent::ChangeLocation(
            ToolbarItemLocation::Hidden,
        ));
        cx.notify();
    }

    pub fn deploy(
        &mut self,
        deploy: &Deploy,
        seed_query_override: Option<SeedQuerySetting>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> bool {
        let filtered_search_range = if deploy.selection_search_enabled {
            Some(FilteredSearchRange::Default)
        } else {
            None
        };
        if self.show(window, cx) {
            if let Some(active_item) = self.active_searchable_item.as_mut() {
                active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
            }
            self.search_suggested(seed_query_override, window, cx);
            self.smartcase(window, cx);
            self.sync_select_next_case_sensitivity(cx);
            self.replace_enabled |= deploy.replace_enabled;
            self.selection_search_enabled =
                self.selection_search_enabled
                    .or(if deploy.selection_search_enabled {
                        Some(FilteredSearchRange::Default)
                    } else {
                        None
                    });
            if deploy.focus {
                let mut handle = self.query_editor.focus_handle(cx);
                let mut select_query = true;

                let has_seed_text = self
                    .query_suggestion(seed_query_override, window, cx)
                    .is_some();
                if deploy.replace_enabled && has_seed_text {
                    handle = self.replacement_editor.focus_handle(cx);
                    select_query = false;
                };

                if select_query {
                    self.select_query(window, cx);
                }

                window.focus(&handle, cx);
            }
            return true;
        }

        cx.propagate();
        false
    }

    pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
        if self.is_dismissed() {
            self.deploy(action, None, window, cx);
        } else {
            self.dismiss(&Dismiss, window, cx);
        }
    }

    pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
        let Some(handle) = self.active_searchable_item.as_ref() else {
            return false;
        };

        let configured_options =
            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
        let settings_changed = configured_options != self.configured_options;

        if self.dismissed && settings_changed {
            // Only update configuration options when search bar is dismissed,
            // so we don't miss updates even after calling show twice
            self.configured_options = configured_options;
            self.search_options = configured_options;
            self.default_options = configured_options;
        }

        // This isn't a normal setting; it's only applicable to vim search.
        self.search_options.remove(SearchOptions::BACKWARDS);

        self.dismissed = false;
        self.adjust_query_regex_language(cx);
        handle.search_bar_visibility_changed(true, window, cx);
        cx.notify();
        cx.emit(Event::UpdateLocation);
        cx.emit(ToolbarItemEvent::ChangeLocation(
            if self.needs_expand_collapse_option(cx) {
                ToolbarItemLocation::PrimaryLeft
            } else {
                ToolbarItemLocation::Secondary
            },
        ));
        true
    }

    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
        self.active_searchable_item
            .as_ref()
            .map(|item| item.supported_options(cx))
            .unwrap_or_default()
    }

    // We provide an expand/collapse button if we are in a multibuffer
    // and not doing a project search.
    fn needs_expand_collapse_option(&self, cx: &App) -> bool {
        if let Some(item) = &self.active_searchable_item {
            let buffer_kind = item.buffer_kind(cx);

            if buffer_kind == ItemBufferKind::Singleton {
                return false;
            }

            let workspace::searchable::SearchOptions {
                find_in_results, ..
            } = item.supported_options(cx);
            !find_in_results
        } else {
            false
        }
    }

    fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context<Self>) {
        self.toggle_fold_all_in_item(window, cx);
    }

    fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(item) = &self.active_searchable_item {
            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
                let editor = item.downcast::<Editor>().expect("Is an editor");
                editor.update(cx, |editor, cx| {
                    let is_collapsed = editor.has_any_buffer_folded(cx);
                    if is_collapsed {
                        editor.unfold_all(&UnfoldAll, window, cx);
                    } else {
                        editor.fold_all(&FoldAll, window, cx);
                    }
                })
            }
        }
    }

    pub fn search_suggested(
        &mut self,
        seed_query_override: Option<SeedQuerySetting>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let search = self
            .query_suggestion(seed_query_override, window, cx)
            .map(|suggestion| {
                self.search(&suggestion, Some(self.default_options), true, window, cx)
            });

        #[cfg(target_os = "macos")]
        let search = search.or_else(|| {
            self.pending_external_query
                .take()
                .map(|(query, options)| self.search(&query, Some(options), true, window, cx))
        });

        if let Some(search) = search {
            cx.spawn_in(window, async move |this, cx| {
                if search.await.is_ok() {
                    this.update_in(cx, |this, window, cx| {
                        if !this.dismissed {
                            this.activate_current_match(window, cx)
                        }
                    })
                } else {
                    Ok(())
                }
            })
            .detach_and_log_err(cx);
        }
    }

    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(match_ix) = self.active_match_index
            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&active_searchable_item.downgrade())
        {
            active_searchable_item.activate_match(match_ix, matches, *token, window, cx)
        }
    }

    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.query_editor.update(cx, |query_editor, cx| {
            query_editor.select_all(&Default::default(), window, cx);
        });
    }

    pub fn query(&self, cx: &App) -> String {
        self.query_editor.read(cx).text(cx)
    }

    pub fn replacement(&self, cx: &mut App) -> String {
        self.replacement_editor.read(cx).text(cx)
    }

    pub fn query_suggestion(
        &mut self,
        seed_query_override: Option<SeedQuerySetting>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Option<String> {
        self.active_searchable_item
            .as_ref()
            .map(|searchable_item| {
                searchable_item.query_suggestion(seed_query_override, window, cx)
            })
            .filter(|suggestion| !suggestion.is_empty())
    }

    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
        if replacement.is_none() {
            self.replace_enabled = false;
            return;
        }
        self.replace_enabled = true;
        self.replacement_editor
            .update(cx, |replacement_editor, cx| {
                replacement_editor
                    .buffer()
                    .update(cx, |replacement_buffer, cx| {
                        let len = replacement_buffer.len(cx);
                        replacement_buffer.edit(
                            [(MultiBufferOffset(0)..len, replacement.unwrap())],
                            None,
                            cx,
                        );
                    });
            });
    }

    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
        cx.notify();
    }

    pub fn search(
        &mut self,
        query: &str,
        options: Option<SearchOptions>,
        add_to_history: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> oneshot::Receiver<()> {
        let options = options.unwrap_or(self.default_options);
        let updated = query != self.query(cx) || self.search_options != options;
        if updated {
            self.query_editor.update(cx, |query_editor, cx| {
                query_editor.buffer().update(cx, |query_buffer, cx| {
                    let len = query_buffer.len(cx);
                    query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
                });
                query_editor.request_autoscroll(Autoscroll::fit(), cx);
            });
            self.set_search_options(options, cx);
            self.clear_matches(window, cx);
            #[cfg(target_os = "macos")]
            self.update_find_pasteboard(cx);
            cx.notify();
        }
        self.update_matches(!updated, add_to_history, window, cx)
    }

    #[cfg(target_os = "macos")]
    pub fn update_find_pasteboard(&mut self, cx: &mut App) {
        cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
            self.query(cx),
            self.search_options.bits().to_string(),
        ));
    }

    pub fn use_selection_for_find(
        &mut self,
        _: &UseSelectionForFind,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.deploy(
            &Deploy {
                focus: false,
                replace_enabled: false,
                selection_search_enabled: false,
            },
            Some(SeedQuerySetting::Always),
            window,
            cx,
        );
    }

    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(active_editor) = self.active_searchable_item.as_ref() {
            let handle = active_editor.item_focus_handle(cx);
            window.focus(&handle, cx);
        }
    }

    pub fn toggle_search_option(
        &mut self,
        search_option: SearchOptions,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.search_options.toggle(search_option);
        self.default_options = self.search_options;
        drop(self.update_matches(false, false, window, cx));
        self.adjust_query_regex_language(cx);
        self.sync_select_next_case_sensitivity(cx);
        cx.notify();
    }

    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
        self.search_options.contains(search_option)
    }

    pub fn enable_search_option(
        &mut self,
        search_option: SearchOptions,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if !self.search_options.contains(search_option) {
            self.toggle_search_option(search_option, window, cx)
        }
    }

    pub fn set_search_within_selection(
        &mut self,
        search_within_selection: Option<FilteredSearchRange>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Option<oneshot::Receiver<()>> {
        let active_item = self.active_searchable_item.as_mut()?;
        self.selection_search_enabled = search_within_selection;
        active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
        cx.notify();
        Some(self.update_matches(false, false, window, cx))
    }

    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
        self.search_options = search_options;
        self.adjust_query_regex_language(cx);
        self.sync_select_next_case_sensitivity(cx);
        cx.notify();
    }

    pub fn clear_search_within_ranges(
        &mut self,
        search_options: SearchOptions,
        cx: &mut Context<Self>,
    ) {
        self.search_options = search_options;
        self.adjust_query_regex_language(cx);
        cx.notify();
    }

    fn select_next_match(
        &mut self,
        _: &SelectNextMatch,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.select_match(Direction::Next, 1, window, cx);
    }

    fn select_prev_match(
        &mut self,
        _: &SelectPreviousMatch,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.select_match(Direction::Prev, 1, window, cx);
    }

    pub fn select_all_matches(
        &mut self,
        _: &SelectAllMatches,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if !self.dismissed
            && self.active_match_index.is_some()
            && let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
        {
            searchable_item.select_matches(matches, *token, window, cx);
            self.focus_editor(&FocusEditor, window, cx);
        }
    }

    pub fn select_match(
        &mut self,
        direction: Direction,
        count: usize,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        #[cfg(target_os = "macos")]
        if let Some((query, options)) = self.pending_external_query.take() {
            let search_rx = self.search(&query, Some(options), true, window, cx);
            cx.spawn_in(window, async move |this, cx| {
                if search_rx.await.is_ok() {
                    this.update_in(cx, |this, window, cx| {
                        this.activate_current_match(window, cx);
                    })
                    .ok();
                }
            })
            .detach();

            return;
        }

        if let Some(index) = self.active_match_index
            && let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
                .filter(|(matches, _)| !matches.is_empty())
        {
            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
            if !EditorSettings::get_global(cx).search_wrap
                && ((direction == Direction::Next && index + count >= matches.len())
                    || (direction == Direction::Prev && index < count))
            {
                crate::show_no_more_matches(window, cx);
                return;
            }
            let new_match_index = searchable_item
                .match_index_for_direction(matches, index, direction, count, *token, window, cx);
            self.active_match_index = Some(new_match_index);

            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
        }
    }

    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
        {
            if matches.is_empty() {
                return;
            }
            searchable_item.update_matches(matches, Some(0), *token, window, cx);
            searchable_item.activate_match(0, matches, *token, window, cx);
        }
    }

    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
        {
            if matches.is_empty() {
                return;
            }
            let new_match_index = matches.len() - 1;
            searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
            searchable_item.activate_match(new_match_index, matches, *token, window, cx);
        }
    }

    fn on_query_editor_event(
        &mut self,
        _editor: &Entity<Editor>,
        event: &editor::EditorEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        match event {
            editor::EditorEvent::Focused => self.query_editor_focused = true,
            editor::EditorEvent::Blurred => self.query_editor_focused = false,
            editor::EditorEvent::Edited { .. } => {
                self.smartcase(window, cx);
                self.clear_matches(window, cx);
                let search = self.update_matches(false, true, window, cx);

                cx.spawn_in(window, async move |this, cx| {
                    if search.await.is_ok() {
                        this.update_in(cx, |this, window, cx| {
                            this.activate_current_match(window, cx);
                            #[cfg(target_os = "macos")]
                            this.update_find_pasteboard(cx);
                        })?;
                    }
                    anyhow::Ok(())
                })
                .detach_and_log_err(cx);
            }
            _ => {}
        }
    }

    fn on_replacement_editor_event(
        &mut self,
        _: Entity<Editor>,
        event: &editor::EditorEvent,
        _: &mut Context<Self>,
    ) {
        match event {
            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
            _ => {}
        }
    }

    fn on_active_searchable_item_event(
        &mut self,
        event: &SearchEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        match event {
            SearchEvent::MatchesInvalidated => {
                drop(self.update_matches(false, false, window, cx));
            }
            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
        }
    }

    fn toggle_case_sensitive(
        &mut self,
        _: &ToggleCaseSensitive,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
    }

    fn toggle_whole_word(
        &mut self,
        _: &ToggleWholeWord,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
    }

    fn toggle_selection(
        &mut self,
        _: &ToggleSelection,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.set_search_within_selection(
            if let Some(_) = self.selection_search_enabled {
                None
            } else {
                Some(FilteredSearchRange::Default)
            },
            window,
            cx,
        );
    }

    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
        self.toggle_search_option(SearchOptions::REGEX, window, cx)
    }

    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
            self.active_match_index = None;
            self.searchable_items_with_matches
                .remove(&active_searchable_item.downgrade());
            active_searchable_item.clear_matches(window, cx);
        }
    }

    pub fn has_active_match(&self) -> bool {
        self.active_match_index.is_some()
    }

    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let mut active_item_matches = None;
        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
            if let Some(searchable_item) =
                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
            {
                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
                    active_item_matches = Some((searchable_item.downgrade(), matches));
                } else {
                    searchable_item.clear_matches(window, cx);
                }
            }
        }

        self.searchable_items_with_matches
            .extend(active_item_matches);
    }

    fn update_matches(
        &mut self,
        reuse_existing_query: bool,
        add_to_history: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> oneshot::Receiver<()> {
        let (done_tx, done_rx) = oneshot::channel();
        let query = self.query(cx);
        self.pending_search.take();
        #[cfg(target_os = "macos")]
        self.pending_external_query.take();

        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
            self.query_error = None;
            if query.is_empty() {
                self.clear_active_searchable_item_matches(window, cx);
                let _ = done_tx.send(());
                cx.notify();
            } else {
                let query: Arc<_> = if let Some(search) =
                    self.active_search.take().filter(|_| reuse_existing_query)
                {
                    search
                } else {
                    // Value doesn't matter, we only construct empty matchers with it

                    if self.search_options.contains(SearchOptions::REGEX) {
                        match SearchQuery::regex(
                            query,
                            self.search_options.contains(SearchOptions::WHOLE_WORD),
                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                            false,
                            self.search_options
                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
                            PathMatcher::default(),
                            PathMatcher::default(),
                            false,
                            None,
                        ) {
                            Ok(query) => query.with_replacement(self.replacement(cx)),
                            Err(e) => {
                                self.query_error = Some(e.to_string());
                                self.clear_active_searchable_item_matches(window, cx);
                                cx.notify();
                                return done_rx;
                            }
                        }
                    } else {
                        match SearchQuery::text(
                            query,
                            self.search_options.contains(SearchOptions::WHOLE_WORD),
                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                            false,
                            PathMatcher::default(),
                            PathMatcher::default(),
                            false,
                            None,
                        ) {
                            Ok(query) => query.with_replacement(self.replacement(cx)),
                            Err(e) => {
                                self.query_error = Some(e.to_string());
                                self.clear_active_searchable_item_matches(window, cx);
                                cx.notify();
                                return done_rx;
                            }
                        }
                    }
                    .into()
                };

                self.active_search = Some(query.clone());
                let query_text = query.as_str().to_string();

                let matches_with_token =
                    active_searchable_item.find_matches_with_token(query, window, cx);

                let active_searchable_item = active_searchable_item.downgrade();
                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
                    let (matches, token) = matches_with_token.await;

                    this.update_in(cx, |this, window, cx| {
                        if let Some(active_searchable_item) =
                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
                        {
                            this.searchable_items_with_matches
                                .insert(active_searchable_item.downgrade(), (matches, token));

                            this.update_match_index(window, cx);

                            if add_to_history {
                                this.search_history
                                    .add(&mut this.search_history_cursor, query_text);
                            }
                            if !this.dismissed {
                                let (matches, token) = this
                                    .searchable_items_with_matches
                                    .get(&active_searchable_item.downgrade())
                                    .unwrap();
                                if matches.is_empty() {
                                    active_searchable_item.clear_matches(window, cx);
                                } else {
                                    active_searchable_item.update_matches(
                                        matches,
                                        this.active_match_index,
                                        *token,
                                        window,
                                        cx,
                                    );
                                }
                            }
                            let _ = done_tx.send(());
                            cx.notify();
                        }
                    })
                    .log_err();
                }));
            }
        }
        done_rx
    }

    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
        if self.search_options.contains(SearchOptions::BACKWARDS) {
            direction.opposite()
        } else {
            direction
        }
    }

    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let direction = self.reverse_direction_if_backwards(Direction::Next);
        let new_index = self
            .active_searchable_item
            .as_ref()
            .and_then(|searchable_item| {
                let (matches, token) = self
                    .searchable_items_with_matches
                    .get(&searchable_item.downgrade())?;
                searchable_item.active_match_index(direction, matches, *token, window, cx)
            });
        if new_index != self.active_match_index {
            self.active_match_index = new_index;
            if !self.dismissed {
                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
                    if let Some((matches, token)) = self
                        .searchable_items_with_matches
                        .get(&searchable_item.downgrade())
                    {
                        if !matches.is_empty() {
                            searchable_item.update_matches(matches, new_index, *token, window, cx);
                        }
                    }
                }
            }
            cx.notify();
        }
    }

    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
        self.cycle_field(Direction::Next, window, cx);
    }

    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
        self.cycle_field(Direction::Prev, window, cx);
    }
    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
        let mut handles = vec![self.query_editor.focus_handle(cx)];
        if self.replace_enabled {
            handles.push(self.replacement_editor.focus_handle(cx));
        }
        if let Some(item) = self.active_searchable_item.as_ref() {
            handles.push(item.item_focus_handle(cx));
        }
        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
            Some(index) => index,
            None => return,
        };

        let new_index = match direction {
            Direction::Next => (current_index + 1) % handles.len(),
            Direction::Prev if current_index == 0 => handles.len() - 1,
            Direction::Prev => (current_index - 1) % handles.len(),
        };
        let next_focus_handle = &handles[new_index];
        self.focus(next_focus_handle, window, cx);
        cx.stop_propagation();
    }

    fn next_history_query(
        &mut self,
        _: &NextHistoryQuery,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Next, cx) {
            cx.propagate();
            return;
        }

        if let Some(new_query) = self
            .search_history
            .next(&mut self.search_history_cursor)
            .map(str::to_string)
        {
            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
        } else if let Some(draft) = self.search_history_cursor.take_draft() {
            drop(self.search(&draft, Some(self.search_options), false, window, cx));
        }
    }

    fn previous_history_query(
        &mut self,
        _: &PreviousHistoryQuery,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Previous, cx) {
            cx.propagate();
            return;
        }

        if self.query(cx).is_empty()
            && let Some(new_query) = self
                .search_history
                .current(&self.search_history_cursor)
                .map(str::to_string)
        {
            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
            return;
        }

        let current_query = self.query(cx);
        if let Some(new_query) = self
            .search_history
            .previous(&mut self.search_history_cursor, &current_query)
            .map(str::to_string)
        {
            drop(self.search(&new_query, Some(self.search_options), false, window, cx));
        }
    }

    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
        window.invalidate_character_coordinates();
        window.focus(handle, cx);
    }

    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
        if self.active_searchable_item.is_some() {
            self.replace_enabled = !self.replace_enabled;
            let handle = if self.replace_enabled {
                self.replacement_editor.focus_handle(cx)
            } else {
                self.query_editor.focus_handle(cx)
            };
            self.focus(&handle, window, cx);
            cx.notify();
        }
    }

    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
        let mut should_propagate = true;
        if !self.dismissed
            && self.active_search.is_some()
            && let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some(query) = self.active_search.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
        {
            if let Some(active_index) = self.active_match_index {
                let query = query
                    .as_ref()
                    .clone()
                    .with_replacement(self.replacement(cx));
                searchable_item.replace(matches.at(active_index), &query, *token, window, cx);
                self.select_next_match(&SelectNextMatch, window, cx);
            }
            should_propagate = false;
        }
        if !should_propagate {
            cx.stop_propagation();
        }
    }

    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
        if !self.dismissed
            && self.active_search.is_some()
            && let Some(searchable_item) = self.active_searchable_item.as_ref()
            && let Some(query) = self.active_search.as_ref()
            && let Some((matches, token)) = self
                .searchable_items_with_matches
                .get(&searchable_item.downgrade())
        {
            let query = query
                .as_ref()
                .clone()
                .with_replacement(self.replacement(cx));
            searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx);
        }
    }

    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
        self.update_match_index(window, cx);
        self.active_match_index.is_some()
    }

    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
        EditorSettings::get_global(cx).use_smartcase_search
    }

    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
        str.chars().any(|c| c.is_uppercase())
    }

    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if self.should_use_smartcase_search(cx) {
            let query = self.query(cx);
            if !query.is_empty() {
                let is_case = self.is_contains_uppercase(&query);
                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
                }
            }
        }
    }

    fn adjust_query_regex_language(&self, cx: &mut App) {
        let enable = self.search_options.contains(SearchOptions::REGEX);
        let query_buffer = self
            .query_editor
            .read(cx)
            .buffer()
            .read(cx)
            .as_singleton()
            .expect("query editor should be backed by a singleton buffer");

        if enable {
            if let Some(regex_language) = self.regex_language.clone() {
                query_buffer.update(cx, |query_buffer, cx| {
                    query_buffer.set_language(Some(regex_language), cx);
                })
            }
        } else {
            query_buffer.update(cx, |query_buffer, cx| {
                query_buffer.set_language(None, cx);
            })
        }
    }

    /// Updates the searchable item's case sensitivity option to match the
    /// search bar's current case sensitivity setting. This ensures that
    /// editor's `select_next`/ `select_previous` operations respect the buffer
    /// search bar's search options.
    ///
    /// Clears the case sensitivity when the search bar is dismissed so that
    /// only the editor's settings are respected.
    fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
        let case_sensitive = match self.dismissed {
            true => None,
            false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
        };

        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
            active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
        }
    }
}

#[cfg(test)]
mod tests {
    use std::{ops::Range, time::Duration};

    use super::*;
    use editor::{
        DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
        SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects,
        display_map::DisplayRow, test::editor_test_context::EditorTestContext,
    };
    use futures::stream::StreamExt as _;
    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
    use language::{Buffer, Point};
    #[cfg(target_os = "macos")]
    use project::Project;
    use settings::{SearchSettingsContent, SettingsStore};
    use unindent::Unindent as _;
    use util_macros::perf;
    #[cfg(target_os = "macos")]
    use workspace::{AppState, MultiWorkspace, Workspace};

    fn init_globals(cx: &mut TestAppContext) {
        cx.update(|cx| {
            let store = settings::SettingsStore::test(cx);
            cx.set_global(store);
            editor::init(cx);

            theme_settings::init(theme::LoadThemes::JustBase, cx);
            crate::init(cx);
        });
    }

    fn init_multibuffer_test(
        cx: &mut TestAppContext,
    ) -> (
        Entity<Editor>,
        Entity<BufferSearchBar>,
        &mut VisualTestContext,
    ) {
        init_globals(cx);

        let buffer1 = cx.new(|cx| {
            Buffer::local(
                            r#"
                            A regular expression (shortened as regex or regexp;[1] also referred to as
                            rational expression[2][3]) is a sequence of characters that specifies a search
                            pattern in text. Usually such patterns are used by string-searching algorithms
                            for "find" or "find and replace" operations on strings, or for input validation.
                            "#
                            .unindent(),
                            cx,
                        )
        });

        let buffer2 = cx.new(|cx| {
            Buffer::local(
                r#"
                            Some Additional text with the term regular expression in it.
                            There two lines.
                            "#
                .unindent(),
                cx,
            )
        });

        let multibuffer = cx.new(|cx| {
            let mut buffer = MultiBuffer::new(language::Capability::ReadWrite);

            //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))]
            buffer.set_excerpts_for_path(
                PathKey::sorted(0),
                buffer1,
                [Point::new(0, 0)..Point::new(3, 0)],
                0,
                cx,
            );
            buffer.set_excerpts_for_path(
                PathKey::sorted(1),
                buffer2,
                [Point::new(0, 0)..Point::new(1, 0)],
                0,
                cx,
            );

            buffer
        });
        let mut editor = None;
        let window = cx.add_window(|window, cx| {
            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
                "keymaps/default-macos.json",
                cx,
            )
            .unwrap();
            cx.bind_keys(default_key_bindings);
            editor =
                Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx)));

            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
            search_bar.show(window, cx);
            search_bar
        });
        let search_bar = window.root(cx).unwrap();

        let cx = VisualTestContext::from_window(*window, cx).into_mut();

        (editor.unwrap(), search_bar, cx)
    }

    fn init_test(
        cx: &mut TestAppContext,
    ) -> (
        Entity<Editor>,
        Entity<BufferSearchBar>,
        &mut VisualTestContext,
    ) {
        init_globals(cx);
        let buffer = cx.new(|cx| {
            Buffer::local(
                r#"
                A regular expression (shortened as regex or regexp;[1] also referred to as
                rational expression[2][3]) is a sequence of characters that specifies a search
                pattern in text. Usually such patterns are used by string-searching algorithms
                for "find" or "find and replace" operations on strings, or for input validation.
                "#
                .unindent(),
                cx,
            )
        });
        let mut editor = None;
        let window = cx.add_window(|window, cx| {
            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
                "keymaps/default-macos.json",
                cx,
            )
            .unwrap();
            cx.bind_keys(default_key_bindings);
            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
            search_bar.show(window, cx);
            search_bar
        });
        let search_bar = window.root(cx).unwrap();

        let cx = VisualTestContext::from_window(*window, cx).into_mut();

        (editor.unwrap(), search_bar, cx)
    }

    #[perf]
    #[gpui::test]
    async fn test_search_simple(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);
        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
            background_highlights
                .into_iter()
                .map(|(range, _)| range)
                .collect::<Vec<_>>()
        };
        // Search for a string that appears with different casing.
        // By default, search is case-insensitive.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("us", None, true, window, cx)
            })
            .await
            .unwrap();
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[
                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
                ]
            );
        });

        // Switch to a case sensitive search.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
        });
        let mut editor_notifications = cx.notifications(&editor);
        editor_notifications.next().await;
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
            );
        });

        // Search for a string that appears both as a whole word and
        // within other words. By default, all results are found.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("or", None, true, window, cx)
            })
            .await
            .unwrap();
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[
                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
                ]
            );
        });

        // Switch to a whole word search.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
        });
        let mut editor_notifications = cx.notifications(&editor);
        editor_notifications.next().await;
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[
                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
                ]
            );
        });

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            assert_eq!(search_bar.active_match_index, Some(0));
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(0));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(1));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(2));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(0));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(2));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(1));
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(0));
        });

        // Park the cursor in between matches and ensure that going to the previous match
        // selects the closest match to the left of the cursor.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(0));
        });

        // Park the cursor in between matches and ensure that going to the next match
        // selects the closest match to the right of the cursor.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(1));
        });

        // Park the cursor after the last match and ensure that going to the previous match
        // selects the last match.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(2));
        });

        // Park the cursor after the last match and ensure that going to the next match
        // wraps around and selects the first match.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(0));
        });

        // Park the cursor before the first match and ensure that going to the previous match
        // wraps around and selects the last match.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
                ])
            });
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            assert_eq!(search_bar.active_match_index, Some(0));
            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
            assert_eq!(
                editor.update(cx, |editor, cx| editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))),
                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
            );
        });
        search_bar.read_with(cx, |search_bar, _| {
            assert_eq!(search_bar.active_match_index, Some(2));
        });
    }

    fn display_points_of(
        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
    ) -> Vec<Range<DisplayPoint>> {
        background_highlights
            .into_iter()
            .map(|(range, _)| range)
            .collect::<Vec<_>>()
    }

    #[perf]
    #[gpui::test]
    async fn test_search_option_handling(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);

        // show with options should make current search case sensitive
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.show(window, cx);
                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
            })
            .await
            .unwrap();
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
            );
        });

        // search_suggested should restore default options
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.search_suggested(None, window, cx);
            assert_eq!(search_bar.search_options, SearchOptions::NONE)
        });

        // toggling a search option should update the defaults
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(
                    "regex",
                    Some(SearchOptions::CASE_SENSITIVE),
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
        });
        let mut editor_notifications = cx.notifications(&editor);
        editor_notifications.next().await;
        editor.update_in(cx, |editor, window, cx| {
            assert_eq!(
                display_points_of(editor.all_text_background_highlights(window, cx)),
                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
            );
        });

        // defaults should still include whole word
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.search_suggested(None, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
            )
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
        init_globals(cx);
        let buffer_text = r#"
        A regular expression (shortened as regex or regexp;[1] also referred to as
        rational expression[2][3]) is a sequence of characters that specifies a search
        pattern in text. Usually such patterns are used by string-searching algorithms
        for "find" or "find and replace" operations on strings, or for input validation.
        "#
        .unindent();
        let expected_query_matches_count = buffer_text
            .chars()
            .filter(|c| c.eq_ignore_ascii_case(&'a'))
            .count();
        assert!(
            expected_query_matches_count > 1,
            "Should pick a query with multiple results"
        );
        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
        let window = cx.add_window(|_, _| gpui::Empty);

        let editor = window.build_entity(cx, |window, cx| {
            Editor::for_buffer(buffer.clone(), None, window, cx)
        });

        let search_bar = window.build_entity(cx, |window, cx| {
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor), window, cx);
            search_bar.show(window, cx);
            search_bar
        });

        window
            .update(cx, |_, window, cx| {
                search_bar.update(cx, |search_bar, cx| {
                    search_bar.search("a", None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        let initial_selections = window
            .update(cx, |_, window, cx| {
                search_bar.update(cx, |search_bar, cx| {
                    let handle = search_bar.query_editor.focus_handle(cx);
                    window.focus(&handle, cx);
                    search_bar.activate_current_match(window, cx);
                });
                assert!(
                    !editor.read(cx).is_focused(window),
                    "Initially, the editor should not be focused"
                );
                let initial_selections = editor.update(cx, |editor, cx| {
                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
                    assert_eq!(
                        initial_selections.len(), 1,
                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
                    );
                    initial_selections
                });
                search_bar.update(cx, |search_bar, cx| {
                    assert_eq!(search_bar.active_match_index, Some(0));
                    let handle = search_bar.query_editor.focus_handle(cx);
                    window.focus(&handle, cx);
                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
                });
                assert!(
                    editor.read(cx).is_focused(window),
                    "Should focus editor after successful SelectAllMatches"
                );
                search_bar.update(cx, |search_bar, cx| {
                    let all_selections =
                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
                    assert_eq!(
                        all_selections.len(),
                        expected_query_matches_count,
                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
                    );
                    assert_eq!(
                        search_bar.active_match_index,
                        Some(0),
                        "Match index should not change after selecting all matches"
                    );
                });

                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
                initial_selections
            }).unwrap();

        window
            .update(cx, |_, window, cx| {
                assert!(
                    editor.read(cx).is_focused(window),
                    "Should still have editor focused after SelectNextMatch"
                );
                search_bar.update(cx, |search_bar, cx| {
                    let all_selections = editor.update(cx, |editor, cx| {
                        editor
                            .selections
                            .display_ranges(&editor.display_snapshot(cx))
                    });
                    assert_eq!(
                        all_selections.len(),
                        1,
                        "On next match, should deselect items and select the next match"
                    );
                    assert_ne!(
                        all_selections, initial_selections,
                        "Next match should be different from the first selection"
                    );
                    assert_eq!(
                        search_bar.active_match_index,
                        Some(1),
                        "Match index should be updated to the next one"
                    );
                    let handle = search_bar.query_editor.focus_handle(cx);
                    window.focus(&handle, cx);
                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
                });
            })
            .unwrap();
        window
            .update(cx, |_, window, cx| {
                assert!(
                    editor.read(cx).is_focused(window),
                    "Should focus editor after successful SelectAllMatches"
                );
                search_bar.update(cx, |search_bar, cx| {
                    let all_selections =
                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
                    assert_eq!(
                    all_selections.len(),
                    expected_query_matches_count,
                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
                );
                    assert_eq!(
                        search_bar.active_match_index,
                        Some(1),
                        "Match index should not change after selecting all matches"
                    );
                });
                search_bar.update(cx, |search_bar, cx| {
                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
                });
            })
            .unwrap();
        let last_match_selections = window
            .update(cx, |_, window, cx| {
                assert!(
                    editor.read(cx).is_focused(window),
                    "Should still have editor focused after SelectPreviousMatch"
                );

                search_bar.update(cx, |search_bar, cx| {
                    let all_selections = editor.update(cx, |editor, cx| {
                        editor
                            .selections
                            .display_ranges(&editor.display_snapshot(cx))
                    });
                    assert_eq!(
                        all_selections.len(),
                        1,
                        "On previous match, should deselect items and select the previous item"
                    );
                    assert_eq!(
                        all_selections, initial_selections,
                        "Previous match should be the same as the first selection"
                    );
                    assert_eq!(
                        search_bar.active_match_index,
                        Some(0),
                        "Match index should be updated to the previous one"
                    );
                    all_selections
                })
            })
            .unwrap();

        window
            .update(cx, |_, window, cx| {
                search_bar.update(cx, |search_bar, cx| {
                    let handle = search_bar.query_editor.focus_handle(cx);
                    window.focus(&handle, cx);
                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
                })
            })
            .unwrap()
            .await
            .unwrap();
        window
            .update(cx, |_, window, cx| {
                search_bar.update(cx, |search_bar, cx| {
                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
                });
                assert!(
                    editor.update(cx, |this, _cx| !this.is_focused(window)),
                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
                );
                search_bar.update(cx, |search_bar, cx| {
                    let all_selections =
                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
                    assert_eq!(
                        all_selections, last_match_selections,
                        "Should not select anything new if there are no matches"
                    );
                    assert!(
                        search_bar.active_match_index.is_none(),
                        "For no matches, there should be no active match index"
                    );
                });
            })
            .unwrap();
    }

    #[perf]
    #[gpui::test]
    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
        init_globals(cx);
        let buffer_text = r#"
        self.buffer.update(cx, |buffer, cx| {
            buffer.edit(
                edits,
                Some(AutoindentMode::Block {
                    original_indent_columns,
                }),
                cx,
            )
        });

        this.buffer.update(cx, |buffer, cx| {
            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
        });
        "#
        .unindent();
        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
        let cx = cx.add_empty_window();

        let editor =
            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));

        let search_bar = cx.new_window_entity(|window, cx| {
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor), window, cx);
            search_bar.show(window, cx);
            search_bar
        });

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(
                    "edit\\(",
                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_all_matches(&SelectAllMatches, window, cx);
        });
        search_bar.update(cx, |_, cx| {
            let all_selections = editor.update(cx, |editor, cx| {
                editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))
            });
            assert_eq!(
                all_selections.len(),
                2,
                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
            );
        });

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(
                    "edit(",
                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_all_matches(&SelectAllMatches, window, cx);
        });
        search_bar.update(cx, |_, cx| {
            let all_selections = editor.update(cx, |editor, cx| {
                editor
                    .selections
                    .display_ranges(&editor.display_snapshot(cx))
            });
            assert_eq!(
                all_selections.len(),
                2,
                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
            );
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_search_query_history(cx: &mut TestAppContext) {
        let (_editor, search_bar, cx) = init_test(cx);

        // Add 3 search items into the history.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("a", None, true, window, cx)
            })
            .await
            .unwrap();
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("b", None, true, window, cx)
            })
            .await
            .unwrap();
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
            })
            .await
            .unwrap();
        // Ensure that the latest search is active.
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "c");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        // Next history query after the latest should preserve the current query.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "c");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "c");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        // Previous query should navigate backwards through history.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "b");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        // Further previous items should go over the history in reverse order.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "a");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        // Previous items should never go behind the first history item.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "a");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "a");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        // Next items should go over the history in the original order.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "b");
            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
        });

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("ba", None, true, window, cx)
            })
            .await
            .unwrap();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "ba");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });

        // New search input should add another entry to history and move the selection to the end of the history.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "c");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "b");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "c");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "ba");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.next_history_query(&NextHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "ba");
            assert_eq!(search_bar.search_options, SearchOptions::NONE);
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_search_query_history_autoscroll(cx: &mut TestAppContext) {
        let (_editor, search_bar, cx) = init_test(cx);

        // Add a long multi-line query that exceeds the editor's max
        // visible height (4 lines), then a short query.
        let long_query = "line1\nline2\nline3\nline4\nline5\nline6";
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(long_query, None, true, window, cx)
            })
            .await
            .unwrap();
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("short", None, true, window, cx)
            })
            .await
            .unwrap();

        // Navigate back to the long entry. Since "short" is single-line,
        // the history navigation is allowed.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
        });
        cx.background_executor.run_until_parked();
        search_bar.update(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), long_query);
        });

        // The cursor should be scrolled into view despite the content
        // exceeding the editor's max visible height.
        search_bar.update_in(cx, |search_bar, window, cx| {
            let snapshot = search_bar
                .query_editor
                .update(cx, |editor, cx| editor.snapshot(window, cx));
            let cursor_row = search_bar
                .query_editor
                .read(cx)
                .selections
                .newest_display(&snapshot)
                .head()
                .row();
            let scroll_top = search_bar
                .query_editor
                .update(cx, |editor, cx| editor.scroll_position(cx).y);
            let visible_lines = search_bar
                .query_editor
                .read(cx)
                .visible_line_count()
                .unwrap_or(0.0);
            let scroll_bottom = scroll_top + visible_lines;
            assert!(
                (cursor_row.0 as f64) < scroll_bottom,
                "cursor row {cursor_row:?} should be visible (scroll range {scroll_top}..{scroll_bottom})"
            );
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_replace_simple(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("expression", None, true, window, cx)
            })
            .await
            .unwrap();

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.replacement_editor.update(cx, |editor, cx| {
                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
                editor.set_text("expr$1", window, cx);
            });
            search_bar.replace_all(&ReplaceAll, window, cx)
        });
        assert_eq!(
            editor.read_with(cx, |this, cx| { this.text(cx) }),
            r#"
        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
        rational expr$1[2][3]) is a sequence of characters that specifies a search
        pattern in text. Usually such patterns are used by string-searching algorithms
        for "find" or "find and replace" operations on strings, or for input validation.
        "#
            .unindent()
        );

        // Search for word boundaries and replace just a single one.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
            })
            .await
            .unwrap();

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.replacement_editor.update(cx, |editor, cx| {
                editor.set_text("banana", window, cx);
            });
            search_bar.replace_next(&ReplaceNext, window, cx)
        });
        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
        assert_eq!(
            editor.read_with(cx, |this, cx| { this.text(cx) }),
            r#"
        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
        rational expr$1[2][3]) is a sequence of characters that specifies a search
        pattern in text. Usually such patterns are used by string-searching algorithms
        for "find" or "find and replace" operations on strings, or for input validation.
        "#
            .unindent()
        );
        // Let's turn on regex mode.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(
                    "\\[([^\\]]+)\\]",
                    Some(SearchOptions::REGEX),
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.replacement_editor.update(cx, |editor, cx| {
                editor.set_text("${1}number", window, cx);
            });
            search_bar.replace_all(&ReplaceAll, window, cx)
        });
        assert_eq!(
            editor.read_with(cx, |this, cx| { this.text(cx) }),
            r#"
        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
        rational expr$12number3number) is a sequence of characters that specifies a search
        pattern in text. Usually such patterns are used by string-searching algorithms
        for "find" or "find and replace" operations on strings, or for input validation.
        "#
            .unindent()
        );
        // Now with a whole-word twist.
        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search(
                    "a\\w+s",
                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.replacement_editor.update(cx, |editor, cx| {
                editor.set_text("things", window, cx);
            });
            search_bar.replace_all(&ReplaceAll, window, cx)
        });
        // The only word affected by this edit should be `algorithms`, even though there's a bunch
        // of words in this text that would match this regex if not for WHOLE_WORD.
        assert_eq!(
            editor.read_with(cx, |this, cx| { this.text(cx) }),
            r#"
        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
        rational expr$12number3number) is a sequence of characters that specifies a search
        pattern in text. Usually such patterns are used by string-searching things
        for "find" or "find and replace" operations on strings, or for input validation.
        "#
            .unindent()
        );
    }

    #[gpui::test]
    async fn test_replace_focus(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);

        editor.update_in(cx, |editor, window, cx| {
            editor.set_text("What a bad day!", window, cx)
        });

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("bad", None, true, window, cx)
            })
            .await
            .unwrap();

        // Calling `toggle_replace` in the search bar ensures that the "Replace
        // *" buttons are rendered, so we can then simulate clicking the
        // buttons.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_replace(&ToggleReplace, window, cx)
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.replacement_editor.update(cx, |editor, cx| {
                editor.set_text("great", window, cx);
            });
        });

        // Focus on the editor instead of the search bar, as we want to ensure
        // that pressing the "Replace Next Match" button will work, even if the
        // search bar is not focused.
        cx.focus(&editor);

        // We'll not simulate clicking the "Replace Next Match " button, asserting that
        // the replacement was done.
        let button_bounds = cx
            .debug_bounds("ICON-ReplaceNext")
            .expect("'Replace Next Match' button should be visible");
        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());

        assert_eq!(
            editor.read_with(cx, |editor, cx| editor.text(cx)),
            "What a great day!"
        );
    }

    struct ReplacementTestParams<'a> {
        editor: &'a Entity<Editor>,
        search_bar: &'a Entity<BufferSearchBar>,
        cx: &'a mut VisualTestContext,
        search_text: &'static str,
        search_options: Option<SearchOptions>,
        replacement_text: &'static str,
        replace_all: bool,
        expected_text: String,
    }

    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
        options
            .search_bar
            .update_in(options.cx, |search_bar, window, cx| {
                if let Some(options) = options.search_options {
                    search_bar.set_search_options(options, cx);
                }
                search_bar.search(
                    options.search_text,
                    options.search_options,
                    true,
                    window,
                    cx,
                )
            })
            .await
            .unwrap();

        options
            .search_bar
            .update_in(options.cx, |search_bar, window, cx| {
                search_bar.replacement_editor.update(cx, |editor, cx| {
                    editor.set_text(options.replacement_text, window, cx);
                });

                if options.replace_all {
                    search_bar.replace_all(&ReplaceAll, window, cx)
                } else {
                    search_bar.replace_next(&ReplaceNext, window, cx)
                }
            });

        assert_eq!(
            options
                .editor
                .read_with(options.cx, |this, cx| { this.text(cx) }),
            options.expected_text
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_replace_special_characters(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);

        run_replacement_test(ReplacementTestParams {
            editor: &editor,
            search_bar: &search_bar,
            cx,
            search_text: "expression",
            search_options: None,
            replacement_text: r"\n",
            replace_all: true,
            expected_text: r#"
            A regular \n (shortened as regex or regexp;[1] also referred to as
            rational \n[2][3]) is a sequence of characters that specifies a search
            pattern in text. Usually such patterns are used by string-searching algorithms
            for "find" or "find and replace" operations on strings, or for input validation.
            "#
            .unindent(),
        })
        .await;

        run_replacement_test(ReplacementTestParams {
            editor: &editor,
            search_bar: &search_bar,
            cx,
            search_text: "or",
            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
            replacement_text: r"\\\n\\\\",
            replace_all: false,
            expected_text: r#"
            A regular \n (shortened as regex \
            \\ regexp;[1] also referred to as
            rational \n[2][3]) is a sequence of characters that specifies a search
            pattern in text. Usually such patterns are used by string-searching algorithms
            for "find" or "find and replace" operations on strings, or for input validation.
            "#
            .unindent(),
        })
        .await;

        run_replacement_test(ReplacementTestParams {
            editor: &editor,
            search_bar: &search_bar,
            cx,
            search_text: r"(that|used) ",
            search_options: Some(SearchOptions::REGEX),
            replacement_text: r"$1\n",
            replace_all: true,
            expected_text: r#"
            A regular \n (shortened as regex \
            \\ regexp;[1] also referred to as
            rational \n[2][3]) is a sequence of characters that
            specifies a search
            pattern in text. Usually such patterns are used
            by string-searching algorithms
            for "find" or "find and replace" operations on strings, or for input validation.
            "#
            .unindent(),
        })
        .await;
    }

    #[gpui::test]
    async fn test_deploy_replace_focuses_replacement_editor(cx: &mut TestAppContext) {
        init_globals(cx);
        let (editor, search_bar, cx) = init_test(cx);

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_display_ranges([
                    DisplayPoint::new(DisplayRow(0), 8)..DisplayPoint::new(DisplayRow(0), 16)
                ])
            });
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.deploy(
                &Deploy {
                    focus: true,
                    replace_enabled: true,
                    selection_search_enabled: false,
                },
                None,
                window,
                cx,
            );
        });
        cx.run_until_parked();

        search_bar.update_in(cx, |search_bar, window, cx| {
            assert!(
                search_bar
                    .replacement_editor
                    .focus_handle(cx)
                    .is_focused(window),
                "replacement editor should be focused when deploying replace with a selection",
            );
            assert!(
                !search_bar.query_editor.focus_handle(cx).is_focused(window),
                "search editor should not be focused when replacement editor is focused",
            );
        });
    }

    #[cfg(target_os = "macos")]
    #[gpui::test]
    async fn test_cmd_e_then_cmd_g_uses_selection_for_find(cx: &mut TestAppContext) {
        init_globals(cx);
        let app_state = cx.update(AppState::test);
        let project = Project::test(app_state.fs.clone(), [], cx).await;
        let buffer = cx.new(|cx| {
            Buffer::local(
                r#"
                dad
                cat
                mom
                dog
                dog
                cat
                dad
                mom
                "#
                .unindent(),
                cx,
            )
        });
        let multibuffer = cx.update(|cx| MultiBuffer::build_from_buffer(buffer, cx));
        let mut editor = None;
        let mut search_bar = None;

        let window = cx.add_window(|window, cx| {
            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
                "keymaps/default-macos.json",
                cx,
            )
            .unwrap();
            cx.bind_keys(default_key_bindings);
            let workspace = cx.new(|cx| Workspace::test_new(project.clone(), window, cx));
            let multi_workspace = MultiWorkspace::new(workspace.clone(), window, cx);
            let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx));
            workspace.update(cx, |workspace, cx| {
                workspace.active_pane().update(cx, |pane, cx| {
                    pane.toolbar().update(cx, |toolbar, cx| {
                        toolbar.add_item(buffer_search_bar.clone(), window, cx);
                    });
                });
            });
            let editor_handle = cx.new(|cx| {
                Editor::new(
                    editor::EditorMode::full(),
                    multibuffer.clone(),
                    Some(project.clone()),
                    window,
                    cx,
                )
            });
            workspace.update(cx, |workspace, cx| {
                workspace.add_item_to_center(Box::new(editor_handle.clone()), window, cx);
            });
            window.focus(&editor_handle.focus_handle(cx), cx);
            search_bar = Some(buffer_search_bar);
            editor = Some(editor_handle);
            multi_workspace
        });
        let cx = VisualTestContext::from_window(*window, cx).into_mut();
        let editor = editor.unwrap();
        let search_bar = search_bar.unwrap();

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
                selections.select_display_ranges([
                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
                ]);
            });
        });

        cx.simulate_keystrokes("cmd-e");

        search_bar.read_with(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "dog");
            assert_eq!(search_bar.active_match_index, Some(0));
        });
        cx.read(|cx| {
            assert_eq!(
                cx.read_from_find_pasteboard().and_then(|item| item.text()),
                Some("dog".to_string())
            );
        });

        cx.simulate_keystrokes("cmd-g");
        assert_eq!(
            editor.update(cx, |editor, cx| editor
                .selections
                .display_ranges(&editor.display_snapshot(cx))),
            [DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 3)]
        );

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
                selections.select_display_ranges([
                    DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)
                ]);
            });
        });

        cx.simulate_keystrokes("cmd-e");

        search_bar.read_with(cx, |search_bar, cx| {
            assert_eq!(search_bar.query(cx), "cat");
            assert_eq!(search_bar.active_match_index, Some(0));
        });
        cx.read(|cx| {
            assert_eq!(
                cx.read_from_find_pasteboard().and_then(|item| item.text()),
                Some("cat".to_string())
            );
        });

        cx.simulate_keystrokes("cmd-g");
        assert_eq!(
            editor.update(cx, |editor, cx| editor
                .selections
                .display_ranges(&editor.display_snapshot(cx))),
            [DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 3)]
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
        cx: &mut TestAppContext,
    ) {
        init_globals(cx);
        let buffer = cx.new(|cx| {
            Buffer::local(
                r#"
                aaa bbb aaa ccc
                aaa bbb aaa ccc
                aaa bbb aaa ccc
                aaa bbb aaa ccc
                aaa bbb aaa ccc
                aaa bbb aaa ccc
                "#
                .unindent(),
                cx,
            )
        });
        let cx = cx.add_empty_window();
        let editor =
            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));

        let search_bar = cx.new_window_entity(|window, cx| {
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor), window, cx);
            search_bar.show(window, cx);
            search_bar
        });

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
            })
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            let deploy = Deploy {
                focus: true,
                replace_enabled: false,
                selection_search_enabled: true,
            };
            search_bar.deploy(&deploy, None, window, cx);
        });

        cx.run_until_parked();

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("aaa", None, true, window, cx)
            })
            .await
            .unwrap();

        editor.update(cx, |editor, cx| {
            assert_eq!(
                editor.search_background_highlights(cx),
                &[
                    Point::new(1, 0)..Point::new(1, 3),
                    Point::new(1, 8)..Point::new(1, 11),
                    Point::new(2, 0)..Point::new(2, 3),
                ]
            );
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
        cx: &mut TestAppContext,
    ) {
        init_globals(cx);
        let text = r#"
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc

            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            aaa bbb aaa ccc
            "#
        .unindent();

        let cx = cx.add_empty_window();
        let editor = cx.new_window_entity(|window, cx| {
            let multibuffer = MultiBuffer::build_multi(
                [
                    (
                        &text,
                        vec![
                            Point::new(0, 0)..Point::new(2, 0),
                            Point::new(4, 0)..Point::new(5, 0),
                        ],
                    ),
                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
                ],
                cx,
            );
            Editor::for_multibuffer(multibuffer, None, window, cx)
        });

        let search_bar = cx.new_window_entity(|window, cx| {
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor), window, cx);
            search_bar.show(window, cx);
            search_bar
        });

        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_ranges(vec![
                    Point::new(1, 0)..Point::new(1, 4),
                    Point::new(5, 3)..Point::new(6, 4),
                ])
            })
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            let deploy = Deploy {
                focus: true,
                replace_enabled: false,
                selection_search_enabled: true,
            };
            search_bar.deploy(&deploy, None, window, cx);
        });

        cx.run_until_parked();

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("aaa", None, true, window, cx)
            })
            .await
            .unwrap();

        editor.update(cx, |editor, cx| {
            assert_eq!(
                editor.search_background_highlights(cx),
                &[
                    Point::new(1, 0)..Point::new(1, 3),
                    Point::new(5, 8)..Point::new(5, 11),
                    Point::new(6, 0)..Point::new(6, 3),
                ]
            );
        });
    }

    #[perf]
    #[gpui::test]
    async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);

        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.set_active_pane_item(Some(&editor), window, cx)
        });

        assert_eq!(initial_location, ToolbarItemLocation::Secondary);

        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.dismiss(&Dismiss, window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
        );

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.show(window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_multibuffer_test(cx);

        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.set_active_pane_item(Some(&editor), window, cx)
        });

        assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft);

        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.dismiss(&Dismiss, window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
        );

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.show(window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::PrimaryLeft))
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_multibuffer_test(cx);

        editor.update(cx, |editor, _| {
            editor.set_in_project_search(true);
        });

        let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.set_active_pane_item(Some(&editor), window, cx)
        });

        assert_eq!(initial_location, ToolbarItemLocation::Hidden);

        let mut events = cx.events::<ToolbarItemEvent, BufferSearchBar>(&search_bar);

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.dismiss(&Dismiss, window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Hidden))
        );

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.show(window, cx);
        });

        assert_eq!(
            events.try_recv().unwrap(),
            (ToolbarItemEvent::ChangeLocation(ToolbarItemLocation::Secondary))
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_multibuffer_test(cx);

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.set_active_pane_item(Some(&editor), window, cx);
        });

        editor.update_in(cx, |editor, window, cx| {
            editor.fold_all(&FoldAll, window, cx);
        });
        cx.run_until_parked();

        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(is_collapsed);

        editor.update_in(cx, |editor, window, cx| {
            editor.unfold_all(&UnfoldAll, window, cx);
        });
        cx.run_until_parked();

        let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(!is_collapsed);
    }

    #[perf]
    #[gpui::test]
    async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_multibuffer_test(cx);

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.set_active_pane_item(Some(&editor), window, cx);
        });

        // Fold all buffers via fold_all
        editor.update_in(cx, |editor, window, cx| {
            editor.fold_all(&FoldAll, window, cx);
        });
        cx.run_until_parked();

        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(
            has_any_folded,
            "All buffers should be folded after fold_all"
        );

        // Manually unfold one buffer (simulating a chevron click)
        let first_buffer_id = editor.read_with(cx, |editor, cx| {
            editor
                .buffer()
                .read(cx)
                .snapshot(cx)
                .excerpts()
                .nth(0)
                .unwrap()
                .context
                .start
                .buffer_id
        });
        editor.update_in(cx, |editor, _window, cx| {
            editor.unfold_buffer(first_buffer_id, cx);
        });

        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(
            has_any_folded,
            "Should still report folds when only one buffer is unfolded"
        );

        // Manually unfold the second buffer too
        let second_buffer_id = editor.read_with(cx, |editor, cx| {
            editor
                .buffer()
                .read(cx)
                .snapshot(cx)
                .excerpts()
                .nth(1)
                .unwrap()
                .context
                .start
                .buffer_id
        });
        editor.update_in(cx, |editor, _window, cx| {
            editor.unfold_buffer(second_buffer_id, cx);
        });

        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(
            !has_any_folded,
            "No folds should remain after unfolding all buffers individually"
        );

        // Manually fold one buffer back
        editor.update_in(cx, |editor, _window, cx| {
            editor.fold_buffer(first_buffer_id, cx);
        });

        let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
        assert!(
            has_any_folded,
            "Should report folds after manually folding one buffer"
        );
    }

    #[perf]
    #[gpui::test]
    async fn test_search_options_changes(cx: &mut TestAppContext) {
        let (_editor, search_bar, cx) = init_test(cx);
        update_search_settings(
            SearchSettings {
                button: true,
                whole_word: false,
                case_sensitive: false,
                include_ignored: false,
                regex: false,
                center_on_match: false,
            },
            cx,
        );

        let deploy = Deploy {
            focus: true,
            replace_enabled: false,
            selection_search_enabled: true,
        };

        search_bar.update_in(cx, |search_bar, window, cx| {
            assert_eq!(
                search_bar.search_options,
                SearchOptions::NONE,
                "Should have no search options enabled by default"
            );
            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::WHOLE_WORD,
                "Should enable the option toggled"
            );
            assert!(
                !search_bar.dismissed,
                "Search bar should be present and visible"
            );
            search_bar.deploy(&deploy, None, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::WHOLE_WORD,
                "After (re)deploying, the option should still be enabled"
            );

            search_bar.dismiss(&Dismiss, window, cx);
            search_bar.deploy(&deploy, None, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::WHOLE_WORD,
                "After hiding and showing the search bar, search options should be preserved"
            );

            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::REGEX,
                "Should enable the options toggled"
            );
            assert!(
                !search_bar.dismissed,
                "Search bar should be present and visible"
            );
            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
        });

        update_search_settings(
            SearchSettings {
                button: true,
                whole_word: false,
                case_sensitive: true,
                include_ignored: false,
                regex: false,
                center_on_match: false,
            },
            cx,
        );
        search_bar.update_in(cx, |search_bar, window, cx| {
            assert_eq!(
                search_bar.search_options,
                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
                "Should have no search options enabled by default"
            );

            search_bar.deploy(&deploy, None, window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
                "Toggling a non-dismissed search bar with custom options should not change the default options"
            );
            search_bar.dismiss(&Dismiss, window, cx);
            search_bar.deploy(&deploy, None, window, cx);
            assert_eq!(
                search_bar.configured_options,
                SearchOptions::CASE_SENSITIVE,
                "After a settings update and toggling the search bar, configured options should be updated"
            );
            assert_eq!(
                search_bar.search_options,
                SearchOptions::CASE_SENSITIVE,
                "After a settings update and toggling the search bar, configured options should be used"
            );
        });

        update_search_settings(
            SearchSettings {
                button: true,
                whole_word: true,
                case_sensitive: true,
                include_ignored: false,
                regex: false,
                center_on_match: false,
            },
            cx,
        );

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.deploy(&deploy, None, window, cx);
            search_bar.dismiss(&Dismiss, window, cx);
            search_bar.show(window, cx);
            assert_eq!(
                search_bar.search_options,
                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
            );
        });
    }

    #[gpui::test]
    async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
        let (editor, search_bar, cx) = init_test(cx);
        let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;

        // Start with case sensitive search settings.
        let mut search_settings = SearchSettings::default();
        search_settings.case_sensitive = true;
        update_search_settings(search_settings, cx);
        search_bar.update(cx, |search_bar, cx| {
            let mut search_options = search_bar.search_options;
            search_options.insert(SearchOptions::CASE_SENSITIVE);
            search_bar.set_search_options(search_options, cx);
        });

        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
        editor_cx.update_editor(|e, window, cx| {
            e.select_next(&Default::default(), window, cx).unwrap();
        });
        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");

        // Update the search bar's case sensitivite toggle, so we can later
        // confirm that `select_next` will now be case-insensitive.
        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_case_sensitive(&Default::default(), window, cx);
        });
        editor_cx.update_editor(|e, window, cx| {
            e.select_next(&Default::default(), window, cx).unwrap();
        });
        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");

        // Confirm that, after dismissing the search bar, only the editor's
        // search settings actually affect the behavior of `select_next`.
        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.dismiss(&Default::default(), window, cx);
        });
        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
        editor_cx.update_editor(|e, window, cx| {
            e.select_next(&Default::default(), window, cx).unwrap();
        });
        editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");

        // Update the editor's search settings, disabling case sensitivity, to
        // check that the value is respected.
        let mut search_settings = SearchSettings::default();
        search_settings.case_sensitive = false;
        update_search_settings(search_settings, cx);
        editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
        editor_cx.update_editor(|e, window, cx| {
            e.select_next(&Default::default(), window, cx).unwrap();
        });
        editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
    }

    #[gpui::test]
    async fn test_regex_search_does_not_highlight_non_matching_occurrences(
        cx: &mut TestAppContext,
    ) {
        init_globals(cx);
        let buffer = cx.new(|cx| {
            Buffer::local(
                "something is at the top\nsomething is behind something\nsomething is at the bottom\n",
                cx,
            )
        });
        let cx = cx.add_empty_window();
        let editor =
            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
        let search_bar = cx.new_window_entity(|window, cx| {
            let mut search_bar = BufferSearchBar::new(None, window, cx);
            search_bar.set_active_pane_item(Some(&editor), window, cx);
            search_bar.show(window, cx);
            search_bar
        });

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
        });

        search_bar
            .update_in(cx, |search_bar, window, cx| {
                search_bar.search("^something", None, true, window, cx)
            })
            .await
            .unwrap();

        search_bar.update_in(cx, |search_bar, window, cx| {
            search_bar.select_next_match(&SelectNextMatch, window, cx);
        });

        // Advance past the debounce so the selection occurrence highlight would
        // have fired if it were not suppressed by the active buffer search.
        cx.executor()
            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
        cx.run_until_parked();

        editor.update(cx, |editor, cx| {
            assert!(
                !editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
                "selection occurrence highlights must be suppressed during buffer search"
            );
            assert_eq!(
                editor.search_background_highlights(cx).len(),
                3,
                "expected exactly 3 search highlights (one per line start)"
            );
        });

        // Manually select "something" — this should restore occurrence highlights
        // because it clears the search-navigation flag.
        editor.update_in(cx, |editor, window, cx| {
            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                s.select_ranges([Point::new(0, 0)..Point::new(0, 9)])
            });
        });

        cx.executor()
            .advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
        cx.run_until_parked();

        editor.update(cx, |editor, _cx| {
            assert!(
                editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
                "selection occurrence highlights must be restored after a manual selection"
            );
        });
    }

    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
        cx.update(|cx| {
            SettingsStore::update_global(cx, |store, cx| {
                store.update_user_settings(cx, |settings| {
                    settings.editor.search = Some(SearchSettingsContent {
                        button: Some(search_settings.button),
                        whole_word: Some(search_settings.whole_word),
                        case_sensitive: Some(search_settings.case_sensitive),
                        include_ignored: Some(search_settings.include_ignored),
                        regex: Some(search_settings.regex),
                        center_on_match: Some(search_settings.center_on_match),
                    });
                });
            });
        });
    }
}
