buffer_search.rs

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