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