buffer_search.rs

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