buffer_search.rs

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