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