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