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