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        let configured_options =
 753            SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 754        let settings_changed = configured_options != self.configured_options;
 755
 756        if self.dismissed && settings_changed {
 757            // Only update configuration options when search bar is dismissed,
 758            // so we don't miss updates even after calling show twice
 759            self.configured_options = configured_options;
 760            self.search_options = configured_options;
 761            self.default_options = configured_options;
 762        }
 763
 764        self.dismissed = false;
 765        self.adjust_query_regex_language(cx);
 766        handle.search_bar_visibility_changed(true, window, cx);
 767        cx.notify();
 768        cx.emit(Event::UpdateLocation);
 769        cx.emit(ToolbarItemEvent::ChangeLocation(
 770            ToolbarItemLocation::Secondary,
 771        ));
 772        true
 773    }
 774
 775    fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
 776        self.active_searchable_item
 777            .as_ref()
 778            .map(|item| item.supported_options(cx))
 779            .unwrap_or_default()
 780    }
 781
 782    pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 783        let search = self
 784            .query_suggestion(window, cx)
 785            .map(|suggestion| self.search(&suggestion, Some(self.default_options), window, cx));
 786
 787        if let Some(search) = search {
 788            cx.spawn_in(window, async move |this, cx| {
 789                search.await?;
 790                this.update_in(cx, |this, window, cx| {
 791                    this.activate_current_match(window, cx)
 792                })
 793            })
 794            .detach_and_log_err(cx);
 795        }
 796    }
 797
 798    pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 799        if let Some(match_ix) = self.active_match_index
 800            && let Some(active_searchable_item) = self.active_searchable_item.as_ref()
 801            && let Some(matches) = self
 802                .searchable_items_with_matches
 803                .get(&active_searchable_item.downgrade())
 804        {
 805            active_searchable_item.activate_match(match_ix, matches, window, cx)
 806        }
 807    }
 808
 809    pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 810        self.query_editor.update(cx, |query_editor, cx| {
 811            query_editor.select_all(&Default::default(), window, cx);
 812        });
 813    }
 814
 815    pub fn query(&self, cx: &App) -> String {
 816        self.query_editor.read(cx).text(cx)
 817    }
 818
 819    pub fn replacement(&self, cx: &mut App) -> String {
 820        self.replacement_editor.read(cx).text(cx)
 821    }
 822
 823    pub fn query_suggestion(
 824        &mut self,
 825        window: &mut Window,
 826        cx: &mut Context<Self>,
 827    ) -> Option<String> {
 828        self.active_searchable_item
 829            .as_ref()
 830            .map(|searchable_item| searchable_item.query_suggestion(window, cx))
 831            .filter(|suggestion| !suggestion.is_empty())
 832    }
 833
 834    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
 835        if replacement.is_none() {
 836            self.replace_enabled = false;
 837            return;
 838        }
 839        self.replace_enabled = true;
 840        self.replacement_editor
 841            .update(cx, |replacement_editor, cx| {
 842                replacement_editor
 843                    .buffer()
 844                    .update(cx, |replacement_buffer, cx| {
 845                        let len = replacement_buffer.len(cx);
 846                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 847                    });
 848            });
 849    }
 850
 851    pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 852        self.focus(&self.replacement_editor.focus_handle(cx), window);
 853        cx.notify();
 854    }
 855
 856    pub fn search(
 857        &mut self,
 858        query: &str,
 859        options: Option<SearchOptions>,
 860        window: &mut Window,
 861        cx: &mut Context<Self>,
 862    ) -> oneshot::Receiver<()> {
 863        let options = options.unwrap_or(self.default_options);
 864        let updated = query != self.query(cx) || self.search_options != options;
 865        if updated {
 866            self.query_editor.update(cx, |query_editor, cx| {
 867                query_editor.buffer().update(cx, |query_buffer, cx| {
 868                    let len = query_buffer.len(cx);
 869                    query_buffer.edit([(0..len, query)], None, cx);
 870                });
 871            });
 872            self.set_search_options(options, cx);
 873            self.clear_matches(window, cx);
 874            cx.notify();
 875        }
 876        self.update_matches(!updated, window, cx)
 877    }
 878
 879    pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
 880        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 881            let handle = active_editor.item_focus_handle(cx);
 882            window.focus(&handle);
 883        }
 884    }
 885
 886    pub fn toggle_search_option(
 887        &mut self,
 888        search_option: SearchOptions,
 889        window: &mut Window,
 890        cx: &mut Context<Self>,
 891    ) {
 892        self.search_options.toggle(search_option);
 893        self.default_options = self.search_options;
 894        drop(self.update_matches(false, window, cx));
 895        self.adjust_query_regex_language(cx);
 896        cx.notify();
 897    }
 898
 899    pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
 900        self.search_options.contains(search_option)
 901    }
 902
 903    pub fn enable_search_option(
 904        &mut self,
 905        search_option: SearchOptions,
 906        window: &mut Window,
 907        cx: &mut Context<Self>,
 908    ) {
 909        if !self.search_options.contains(search_option) {
 910            self.toggle_search_option(search_option, window, cx)
 911        }
 912    }
 913
 914    pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
 915        self.search_options = search_options;
 916        self.adjust_query_regex_language(cx);
 917        cx.notify();
 918    }
 919
 920    pub fn clear_search_within_ranges(
 921        &mut self,
 922        search_options: SearchOptions,
 923        cx: &mut Context<Self>,
 924    ) {
 925        self.search_options = search_options;
 926        self.adjust_query_regex_language(cx);
 927        cx.notify();
 928    }
 929
 930    fn select_next_match(
 931        &mut self,
 932        _: &SelectNextMatch,
 933        window: &mut Window,
 934        cx: &mut Context<Self>,
 935    ) {
 936        self.select_match(Direction::Next, 1, window, cx);
 937    }
 938
 939    fn select_prev_match(
 940        &mut self,
 941        _: &SelectPreviousMatch,
 942        window: &mut Window,
 943        cx: &mut Context<Self>,
 944    ) {
 945        self.select_match(Direction::Prev, 1, window, cx);
 946    }
 947
 948    fn select_all_matches(
 949        &mut self,
 950        _: &SelectAllMatches,
 951        window: &mut Window,
 952        cx: &mut Context<Self>,
 953    ) {
 954        if !self.dismissed
 955            && self.active_match_index.is_some()
 956            && let Some(searchable_item) = self.active_searchable_item.as_ref()
 957            && let Some(matches) = self
 958                .searchable_items_with_matches
 959                .get(&searchable_item.downgrade())
 960        {
 961            searchable_item.select_matches(matches, window, cx);
 962            self.focus_editor(&FocusEditor, window, cx);
 963        }
 964    }
 965
 966    pub fn select_match(
 967        &mut self,
 968        direction: Direction,
 969        count: usize,
 970        window: &mut Window,
 971        cx: &mut Context<Self>,
 972    ) {
 973        if let Some(index) = self.active_match_index
 974            && let Some(searchable_item) = self.active_searchable_item.as_ref()
 975            && let Some(matches) = self
 976                .searchable_items_with_matches
 977                .get(&searchable_item.downgrade())
 978                .filter(|matches| !matches.is_empty())
 979        {
 980            // If 'wrapscan' is disabled, searches do not wrap around the end of the file.
 981            if !EditorSettings::get_global(cx).search_wrap
 982                && ((direction == Direction::Next && index + count >= matches.len())
 983                    || (direction == Direction::Prev && index < count))
 984            {
 985                crate::show_no_more_matches(window, cx);
 986                return;
 987            }
 988            let new_match_index = searchable_item
 989                .match_index_for_direction(matches, index, direction, count, window, cx);
 990
 991            searchable_item.update_matches(matches, window, cx);
 992            searchable_item.activate_match(new_match_index, matches, window, cx);
 993        }
 994    }
 995
 996    pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 997        if let Some(searchable_item) = self.active_searchable_item.as_ref()
 998            && let Some(matches) = self
 999                .searchable_items_with_matches
1000                .get(&searchable_item.downgrade())
1001        {
1002            if matches.is_empty() {
1003                return;
1004            }
1005            searchable_item.update_matches(matches, window, cx);
1006            searchable_item.activate_match(0, matches, window, cx);
1007        }
1008    }
1009
1010    pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1011        if let Some(searchable_item) = self.active_searchable_item.as_ref()
1012            && let Some(matches) = self
1013                .searchable_items_with_matches
1014                .get(&searchable_item.downgrade())
1015        {
1016            if matches.is_empty() {
1017                return;
1018            }
1019            let new_match_index = matches.len() - 1;
1020            searchable_item.update_matches(matches, window, cx);
1021            searchable_item.activate_match(new_match_index, matches, window, cx);
1022        }
1023    }
1024
1025    fn on_query_editor_event(
1026        &mut self,
1027        editor: &Entity<Editor>,
1028        event: &editor::EditorEvent,
1029        window: &mut Window,
1030        cx: &mut Context<Self>,
1031    ) {
1032        match event {
1033            editor::EditorEvent::Focused => self.query_editor_focused = true,
1034            editor::EditorEvent::Blurred => self.query_editor_focused = false,
1035            editor::EditorEvent::Edited { .. } => {
1036                self.smartcase(window, cx);
1037                self.clear_matches(window, cx);
1038                let search = self.update_matches(false, window, cx);
1039
1040                let width = editor.update(cx, |editor, cx| {
1041                    let text_layout_details = editor.text_layout_details(window);
1042                    let snapshot = editor.snapshot(window, cx).display_snapshot;
1043
1044                    snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
1045                        - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
1046                });
1047                self.editor_needed_width = width;
1048                cx.notify();
1049
1050                cx.spawn_in(window, async move |this, cx| {
1051                    search.await?;
1052                    this.update_in(cx, |this, window, cx| {
1053                        this.activate_current_match(window, cx)
1054                    })
1055                })
1056                .detach_and_log_err(cx);
1057            }
1058            _ => {}
1059        }
1060    }
1061
1062    fn on_replacement_editor_event(
1063        &mut self,
1064        _: Entity<Editor>,
1065        event: &editor::EditorEvent,
1066        _: &mut Context<Self>,
1067    ) {
1068        match event {
1069            editor::EditorEvent::Focused => self.replacement_editor_focused = true,
1070            editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
1071            _ => {}
1072        }
1073    }
1074
1075    fn on_active_searchable_item_event(
1076        &mut self,
1077        event: &SearchEvent,
1078        window: &mut Window,
1079        cx: &mut Context<Self>,
1080    ) {
1081        match event {
1082            SearchEvent::MatchesInvalidated => {
1083                drop(self.update_matches(false, window, cx));
1084            }
1085            SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
1086        }
1087    }
1088
1089    fn toggle_case_sensitive(
1090        &mut self,
1091        _: &ToggleCaseSensitive,
1092        window: &mut Window,
1093        cx: &mut Context<Self>,
1094    ) {
1095        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
1096    }
1097
1098    fn toggle_whole_word(
1099        &mut self,
1100        _: &ToggleWholeWord,
1101        window: &mut Window,
1102        cx: &mut Context<Self>,
1103    ) {
1104        self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1105    }
1106
1107    fn toggle_selection(
1108        &mut self,
1109        _: &ToggleSelection,
1110        window: &mut Window,
1111        cx: &mut Context<Self>,
1112    ) {
1113        if let Some(active_item) = self.active_searchable_item.as_mut() {
1114            self.selection_search_enabled = !self.selection_search_enabled;
1115            active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
1116            drop(self.update_matches(false, window, cx));
1117            cx.notify();
1118        }
1119    }
1120
1121    fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
1122        self.toggle_search_option(SearchOptions::REGEX, window, cx)
1123    }
1124
1125    fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
1126        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1127            self.active_match_index = None;
1128            self.searchable_items_with_matches
1129                .remove(&active_searchable_item.downgrade());
1130            active_searchable_item.clear_matches(window, cx);
1131        }
1132    }
1133
1134    pub fn has_active_match(&self) -> bool {
1135        self.active_match_index.is_some()
1136    }
1137
1138    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1139        let mut active_item_matches = None;
1140        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
1141            if let Some(searchable_item) =
1142                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
1143            {
1144                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
1145                    active_item_matches = Some((searchable_item.downgrade(), matches));
1146                } else {
1147                    searchable_item.clear_matches(window, cx);
1148                }
1149            }
1150        }
1151
1152        self.searchable_items_with_matches
1153            .extend(active_item_matches);
1154    }
1155
1156    fn update_matches(
1157        &mut self,
1158        reuse_existing_query: bool,
1159        window: &mut Window,
1160        cx: &mut Context<Self>,
1161    ) -> oneshot::Receiver<()> {
1162        let (done_tx, done_rx) = oneshot::channel();
1163        let query = self.query(cx);
1164        self.pending_search.take();
1165
1166        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
1167            self.query_error = None;
1168            if query.is_empty() {
1169                self.clear_active_searchable_item_matches(window, cx);
1170                let _ = done_tx.send(());
1171                cx.notify();
1172            } else {
1173                let query: Arc<_> = if let Some(search) =
1174                    self.active_search.take().filter(|_| reuse_existing_query)
1175                {
1176                    search
1177                } else {
1178                    if self.search_options.contains(SearchOptions::REGEX) {
1179                        match SearchQuery::regex(
1180                            query,
1181                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1182                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1183                            false,
1184                            self.search_options
1185                                .contains(SearchOptions::ONE_MATCH_PER_LINE),
1186                            Default::default(),
1187                            Default::default(),
1188                            false,
1189                            None,
1190                        ) {
1191                            Ok(query) => query.with_replacement(self.replacement(cx)),
1192                            Err(e) => {
1193                                self.query_error = Some(e.to_string());
1194                                self.clear_active_searchable_item_matches(window, cx);
1195                                cx.notify();
1196                                return done_rx;
1197                            }
1198                        }
1199                    } else {
1200                        match SearchQuery::text(
1201                            query,
1202                            self.search_options.contains(SearchOptions::WHOLE_WORD),
1203                            self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1204                            false,
1205                            Default::default(),
1206                            Default::default(),
1207                            false,
1208                            None,
1209                        ) {
1210                            Ok(query) => query.with_replacement(self.replacement(cx)),
1211                            Err(e) => {
1212                                self.query_error = Some(e.to_string());
1213                                self.clear_active_searchable_item_matches(window, cx);
1214                                cx.notify();
1215                                return done_rx;
1216                            }
1217                        }
1218                    }
1219                    .into()
1220                };
1221
1222                self.active_search = Some(query.clone());
1223                let query_text = query.as_str().to_string();
1224
1225                let matches = active_searchable_item.find_matches(query, window, cx);
1226
1227                let active_searchable_item = active_searchable_item.downgrade();
1228                self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
1229                    let matches = matches.await;
1230
1231                    this.update_in(cx, |this, window, cx| {
1232                        if let Some(active_searchable_item) =
1233                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1234                        {
1235                            this.searchable_items_with_matches
1236                                .insert(active_searchable_item.downgrade(), matches);
1237
1238                            this.update_match_index(window, cx);
1239                            this.search_history
1240                                .add(&mut this.search_history_cursor, query_text);
1241                            if !this.dismissed {
1242                                let matches = this
1243                                    .searchable_items_with_matches
1244                                    .get(&active_searchable_item.downgrade())
1245                                    .unwrap();
1246                                if matches.is_empty() {
1247                                    active_searchable_item.clear_matches(window, cx);
1248                                } else {
1249                                    active_searchable_item.update_matches(matches, window, cx);
1250                                }
1251                                let _ = done_tx.send(());
1252                            }
1253                            cx.notify();
1254                        }
1255                    })
1256                    .log_err();
1257                }));
1258            }
1259        }
1260        done_rx
1261    }
1262
1263    fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
1264        if self.search_options.contains(SearchOptions::BACKWARDS) {
1265            direction.opposite()
1266        } else {
1267            direction
1268        }
1269    }
1270
1271    pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1272        let direction = self.reverse_direction_if_backwards(Direction::Next);
1273        let new_index = self
1274            .active_searchable_item
1275            .as_ref()
1276            .and_then(|searchable_item| {
1277                let matches = self
1278                    .searchable_items_with_matches
1279                    .get(&searchable_item.downgrade())?;
1280                searchable_item.active_match_index(direction, matches, window, cx)
1281            });
1282        if new_index != self.active_match_index {
1283            self.active_match_index = new_index;
1284            cx.notify();
1285        }
1286    }
1287
1288    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1289        self.cycle_field(Direction::Next, window, cx);
1290    }
1291
1292    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1293        self.cycle_field(Direction::Prev, window, cx);
1294    }
1295    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1296        let mut handles = vec![self.query_editor.focus_handle(cx)];
1297        if self.replace_enabled {
1298            handles.push(self.replacement_editor.focus_handle(cx));
1299        }
1300        if let Some(item) = self.active_searchable_item.as_ref() {
1301            handles.push(item.item_focus_handle(cx));
1302        }
1303        let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
1304            Some(index) => index,
1305            None => return,
1306        };
1307
1308        let new_index = match direction {
1309            Direction::Next => (current_index + 1) % handles.len(),
1310            Direction::Prev if current_index == 0 => handles.len() - 1,
1311            Direction::Prev => (current_index - 1) % handles.len(),
1312        };
1313        let next_focus_handle = &handles[new_index];
1314        self.focus(next_focus_handle, window);
1315        cx.stop_propagation();
1316    }
1317
1318    fn next_history_query(
1319        &mut self,
1320        _: &NextHistoryQuery,
1321        window: &mut Window,
1322        cx: &mut Context<Self>,
1323    ) {
1324        if let Some(new_query) = self
1325            .search_history
1326            .next(&mut self.search_history_cursor)
1327            .map(str::to_string)
1328        {
1329            drop(self.search(&new_query, Some(self.search_options), window, cx));
1330        } else {
1331            self.search_history_cursor.reset();
1332            drop(self.search("", Some(self.search_options), window, cx));
1333        }
1334    }
1335
1336    fn previous_history_query(
1337        &mut self,
1338        _: &PreviousHistoryQuery,
1339        window: &mut Window,
1340        cx: &mut Context<Self>,
1341    ) {
1342        if self.query(cx).is_empty()
1343            && let Some(new_query) = self
1344                .search_history
1345                .current(&self.search_history_cursor)
1346                .map(str::to_string)
1347        {
1348            drop(self.search(&new_query, Some(self.search_options), window, cx));
1349            return;
1350        }
1351
1352        if let Some(new_query) = self
1353            .search_history
1354            .previous(&mut self.search_history_cursor)
1355            .map(str::to_string)
1356        {
1357            drop(self.search(&new_query, Some(self.search_options), window, cx));
1358        }
1359    }
1360
1361    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
1362        window.invalidate_character_coordinates();
1363        window.focus(handle);
1364    }
1365
1366    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1367        if self.active_searchable_item.is_some() {
1368            self.replace_enabled = !self.replace_enabled;
1369            let handle = if self.replace_enabled {
1370                self.replacement_editor.focus_handle(cx)
1371            } else {
1372                self.query_editor.focus_handle(cx)
1373            };
1374            self.focus(&handle, window);
1375            cx.notify();
1376        }
1377    }
1378
1379    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
1380        let mut should_propagate = true;
1381        if !self.dismissed
1382            && self.active_search.is_some()
1383            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1384            && let Some(query) = self.active_search.as_ref()
1385            && let Some(matches) = self
1386                .searchable_items_with_matches
1387                .get(&searchable_item.downgrade())
1388        {
1389            if let Some(active_index) = self.active_match_index {
1390                let query = query
1391                    .as_ref()
1392                    .clone()
1393                    .with_replacement(self.replacement(cx));
1394                searchable_item.replace(matches.at(active_index), &query, window, cx);
1395                self.select_next_match(&SelectNextMatch, window, cx);
1396            }
1397            should_propagate = false;
1398        }
1399        if !should_propagate {
1400            cx.stop_propagation();
1401        }
1402    }
1403
1404    pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
1405        if !self.dismissed
1406            && self.active_search.is_some()
1407            && let Some(searchable_item) = self.active_searchable_item.as_ref()
1408            && let Some(query) = self.active_search.as_ref()
1409            && let Some(matches) = self
1410                .searchable_items_with_matches
1411                .get(&searchable_item.downgrade())
1412        {
1413            let query = query
1414                .as_ref()
1415                .clone()
1416                .with_replacement(self.replacement(cx));
1417            searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
1418        }
1419    }
1420
1421    pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1422        self.update_match_index(window, cx);
1423        self.active_match_index.is_some()
1424    }
1425
1426    pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
1427        EditorSettings::get_global(cx).use_smartcase_search
1428    }
1429
1430    pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
1431        str.chars().any(|c| c.is_uppercase())
1432    }
1433
1434    fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1435        if self.should_use_smartcase_search(cx) {
1436            let query = self.query(cx);
1437            if !query.is_empty() {
1438                let is_case = self.is_contains_uppercase(&query);
1439                if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
1440                    self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1441                }
1442            }
1443        }
1444    }
1445
1446    fn adjust_query_regex_language(&self, cx: &mut App) {
1447        let enable = self.search_options.contains(SearchOptions::REGEX);
1448        let query_buffer = self
1449            .query_editor
1450            .read(cx)
1451            .buffer()
1452            .read(cx)
1453            .as_singleton()
1454            .expect("query editor should be backed by a singleton buffer");
1455        if enable {
1456            if let Some(regex_language) = self.regex_language.clone() {
1457                query_buffer.update(cx, |query_buffer, cx| {
1458                    query_buffer.set_language(Some(regex_language), cx);
1459                })
1460            }
1461        } else {
1462            query_buffer.update(cx, |query_buffer, cx| {
1463                query_buffer.set_language(None, cx);
1464            })
1465        }
1466    }
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471    use std::ops::Range;
1472
1473    use super::*;
1474    use editor::{
1475        DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
1476        display_map::DisplayRow,
1477    };
1478    use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
1479    use language::{Buffer, Point};
1480    use project::Project;
1481    use settings::SettingsStore;
1482    use smol::stream::StreamExt as _;
1483    use unindent::Unindent as _;
1484
1485    fn init_globals(cx: &mut TestAppContext) {
1486        cx.update(|cx| {
1487            let store = settings::SettingsStore::test(cx);
1488            cx.set_global(store);
1489            workspace::init_settings(cx);
1490            editor::init(cx);
1491
1492            language::init(cx);
1493            Project::init_settings(cx);
1494            theme::init(theme::LoadThemes::JustBase, cx);
1495            crate::init(cx);
1496        });
1497    }
1498
1499    fn init_test(
1500        cx: &mut TestAppContext,
1501    ) -> (
1502        Entity<Editor>,
1503        Entity<BufferSearchBar>,
1504        &mut VisualTestContext,
1505    ) {
1506        init_globals(cx);
1507        let buffer = cx.new(|cx| {
1508            Buffer::local(
1509                r#"
1510                A regular expression (shortened as regex or regexp;[1] also referred to as
1511                rational expression[2][3]) is a sequence of characters that specifies a search
1512                pattern in text. Usually such patterns are used by string-searching algorithms
1513                for "find" or "find and replace" operations on strings, or for input validation.
1514                "#
1515                .unindent(),
1516                cx,
1517            )
1518        });
1519        let cx = cx.add_empty_window();
1520        let editor =
1521            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
1522
1523        let search_bar = cx.new_window_entity(|window, cx| {
1524            let mut search_bar = BufferSearchBar::new(None, window, cx);
1525            search_bar.set_active_pane_item(Some(&editor), window, cx);
1526            search_bar.show(window, cx);
1527            search_bar
1528        });
1529
1530        (editor, search_bar, cx)
1531    }
1532
1533    #[gpui::test]
1534    async fn test_search_simple(cx: &mut TestAppContext) {
1535        let (editor, search_bar, cx) = init_test(cx);
1536        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1537            background_highlights
1538                .into_iter()
1539                .map(|(range, _)| range)
1540                .collect::<Vec<_>>()
1541        };
1542        // Search for a string that appears with different casing.
1543        // By default, search is case-insensitive.
1544        search_bar
1545            .update_in(cx, |search_bar, window, cx| {
1546                search_bar.search("us", None, window, cx)
1547            })
1548            .await
1549            .unwrap();
1550        editor.update_in(cx, |editor, window, cx| {
1551            assert_eq!(
1552                display_points_of(editor.all_text_background_highlights(window, cx)),
1553                &[
1554                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1555                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1556                ]
1557            );
1558        });
1559
1560        // Switch to a case sensitive search.
1561        search_bar.update_in(cx, |search_bar, window, cx| {
1562            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1563        });
1564        let mut editor_notifications = cx.notifications(&editor);
1565        editor_notifications.next().await;
1566        editor.update_in(cx, |editor, window, cx| {
1567            assert_eq!(
1568                display_points_of(editor.all_text_background_highlights(window, cx)),
1569                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1570            );
1571        });
1572
1573        // Search for a string that appears both as a whole word and
1574        // within other words. By default, all results are found.
1575        search_bar
1576            .update_in(cx, |search_bar, window, cx| {
1577                search_bar.search("or", None, window, cx)
1578            })
1579            .await
1580            .unwrap();
1581        editor.update_in(cx, |editor, window, cx| {
1582            assert_eq!(
1583                display_points_of(editor.all_text_background_highlights(window, cx)),
1584                &[
1585                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1586                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1587                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1588                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1589                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1590                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1591                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1592                ]
1593            );
1594        });
1595
1596        // Switch to a whole word search.
1597        search_bar.update_in(cx, |search_bar, window, cx| {
1598            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1599        });
1600        let mut editor_notifications = cx.notifications(&editor);
1601        editor_notifications.next().await;
1602        editor.update_in(cx, |editor, window, cx| {
1603            assert_eq!(
1604                display_points_of(editor.all_text_background_highlights(window, cx)),
1605                &[
1606                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1607                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1608                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1609                ]
1610            );
1611        });
1612
1613        editor.update_in(cx, |editor, window, cx| {
1614            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1615                s.select_display_ranges([
1616                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1617                ])
1618            });
1619        });
1620        search_bar.update_in(cx, |search_bar, window, cx| {
1621            assert_eq!(search_bar.active_match_index, Some(0));
1622            search_bar.select_next_match(&SelectNextMatch, window, cx);
1623            assert_eq!(
1624                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1625                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1626            );
1627        });
1628        search_bar.read_with(cx, |search_bar, _| {
1629            assert_eq!(search_bar.active_match_index, Some(0));
1630        });
1631
1632        search_bar.update_in(cx, |search_bar, window, cx| {
1633            search_bar.select_next_match(&SelectNextMatch, window, cx);
1634            assert_eq!(
1635                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1636                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1637            );
1638        });
1639        search_bar.read_with(cx, |search_bar, _| {
1640            assert_eq!(search_bar.active_match_index, Some(1));
1641        });
1642
1643        search_bar.update_in(cx, |search_bar, window, cx| {
1644            search_bar.select_next_match(&SelectNextMatch, window, cx);
1645            assert_eq!(
1646                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1647                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1648            );
1649        });
1650        search_bar.read_with(cx, |search_bar, _| {
1651            assert_eq!(search_bar.active_match_index, Some(2));
1652        });
1653
1654        search_bar.update_in(cx, |search_bar, window, cx| {
1655            search_bar.select_next_match(&SelectNextMatch, window, cx);
1656            assert_eq!(
1657                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1658                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1659            );
1660        });
1661        search_bar.read_with(cx, |search_bar, _| {
1662            assert_eq!(search_bar.active_match_index, Some(0));
1663        });
1664
1665        search_bar.update_in(cx, |search_bar, window, cx| {
1666            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1667            assert_eq!(
1668                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1669                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1670            );
1671        });
1672        search_bar.read_with(cx, |search_bar, _| {
1673            assert_eq!(search_bar.active_match_index, Some(2));
1674        });
1675
1676        search_bar.update_in(cx, |search_bar, window, cx| {
1677            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1678            assert_eq!(
1679                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1680                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1681            );
1682        });
1683        search_bar.read_with(cx, |search_bar, _| {
1684            assert_eq!(search_bar.active_match_index, Some(1));
1685        });
1686
1687        search_bar.update_in(cx, |search_bar, window, cx| {
1688            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1689            assert_eq!(
1690                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1691                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1692            );
1693        });
1694        search_bar.read_with(cx, |search_bar, _| {
1695            assert_eq!(search_bar.active_match_index, Some(0));
1696        });
1697
1698        // Park the cursor in between matches and ensure that going to the previous match selects
1699        // the closest match to the left.
1700        editor.update_in(cx, |editor, window, cx| {
1701            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1702                s.select_display_ranges([
1703                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1704                ])
1705            });
1706        });
1707        search_bar.update_in(cx, |search_bar, window, cx| {
1708            assert_eq!(search_bar.active_match_index, Some(1));
1709            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1710            assert_eq!(
1711                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1712                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1713            );
1714        });
1715        search_bar.read_with(cx, |search_bar, _| {
1716            assert_eq!(search_bar.active_match_index, Some(0));
1717        });
1718
1719        // Park the cursor in between matches and ensure that going to the next match selects the
1720        // closest match to the right.
1721        editor.update_in(cx, |editor, window, cx| {
1722            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1723                s.select_display_ranges([
1724                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1725                ])
1726            });
1727        });
1728        search_bar.update_in(cx, |search_bar, window, cx| {
1729            assert_eq!(search_bar.active_match_index, Some(1));
1730            search_bar.select_next_match(&SelectNextMatch, window, cx);
1731            assert_eq!(
1732                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1733                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1734            );
1735        });
1736        search_bar.read_with(cx, |search_bar, _| {
1737            assert_eq!(search_bar.active_match_index, Some(1));
1738        });
1739
1740        // Park the cursor after the last match and ensure that going to the previous match selects
1741        // the last match.
1742        editor.update_in(cx, |editor, window, cx| {
1743            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744                s.select_display_ranges([
1745                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1746                ])
1747            });
1748        });
1749        search_bar.update_in(cx, |search_bar, window, cx| {
1750            assert_eq!(search_bar.active_match_index, Some(2));
1751            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1752            assert_eq!(
1753                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1754                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1755            );
1756        });
1757        search_bar.read_with(cx, |search_bar, _| {
1758            assert_eq!(search_bar.active_match_index, Some(2));
1759        });
1760
1761        // Park the cursor after the last match and ensure that going to the next match selects the
1762        // first match.
1763        editor.update_in(cx, |editor, window, cx| {
1764            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1765                s.select_display_ranges([
1766                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1767                ])
1768            });
1769        });
1770        search_bar.update_in(cx, |search_bar, window, cx| {
1771            assert_eq!(search_bar.active_match_index, Some(2));
1772            search_bar.select_next_match(&SelectNextMatch, window, cx);
1773            assert_eq!(
1774                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1775                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1776            );
1777        });
1778        search_bar.read_with(cx, |search_bar, _| {
1779            assert_eq!(search_bar.active_match_index, Some(0));
1780        });
1781
1782        // Park the cursor before the first match and ensure that going to the previous match
1783        // selects the last match.
1784        editor.update_in(cx, |editor, window, cx| {
1785            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1786                s.select_display_ranges([
1787                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1788                ])
1789            });
1790        });
1791        search_bar.update_in(cx, |search_bar, window, cx| {
1792            assert_eq!(search_bar.active_match_index, Some(0));
1793            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1794            assert_eq!(
1795                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1796                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1797            );
1798        });
1799        search_bar.read_with(cx, |search_bar, _| {
1800            assert_eq!(search_bar.active_match_index, Some(2));
1801        });
1802    }
1803
1804    fn display_points_of(
1805        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1806    ) -> Vec<Range<DisplayPoint>> {
1807        background_highlights
1808            .into_iter()
1809            .map(|(range, _)| range)
1810            .collect::<Vec<_>>()
1811    }
1812
1813    #[gpui::test]
1814    async fn test_search_option_handling(cx: &mut TestAppContext) {
1815        let (editor, search_bar, cx) = init_test(cx);
1816
1817        // show with options should make current search case sensitive
1818        search_bar
1819            .update_in(cx, |search_bar, window, cx| {
1820                search_bar.show(window, cx);
1821                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1822            })
1823            .await
1824            .unwrap();
1825        editor.update_in(cx, |editor, window, cx| {
1826            assert_eq!(
1827                display_points_of(editor.all_text_background_highlights(window, cx)),
1828                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1829            );
1830        });
1831
1832        // search_suggested should restore default options
1833        search_bar.update_in(cx, |search_bar, window, cx| {
1834            search_bar.search_suggested(window, cx);
1835            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1836        });
1837
1838        // toggling a search option should update the defaults
1839        search_bar
1840            .update_in(cx, |search_bar, window, cx| {
1841                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), window, cx)
1842            })
1843            .await
1844            .unwrap();
1845        search_bar.update_in(cx, |search_bar, window, cx| {
1846            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1847        });
1848        let mut editor_notifications = cx.notifications(&editor);
1849        editor_notifications.next().await;
1850        editor.update_in(cx, |editor, window, cx| {
1851            assert_eq!(
1852                display_points_of(editor.all_text_background_highlights(window, cx)),
1853                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1854            );
1855        });
1856
1857        // defaults should still include whole word
1858        search_bar.update_in(cx, |search_bar, window, cx| {
1859            search_bar.search_suggested(window, cx);
1860            assert_eq!(
1861                search_bar.search_options,
1862                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1863            )
1864        });
1865    }
1866
1867    #[gpui::test]
1868    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1869        init_globals(cx);
1870        let buffer_text = r#"
1871        A regular expression (shortened as regex or regexp;[1] also referred to as
1872        rational expression[2][3]) is a sequence of characters that specifies a search
1873        pattern in text. Usually such patterns are used by string-searching algorithms
1874        for "find" or "find and replace" operations on strings, or for input validation.
1875        "#
1876        .unindent();
1877        let expected_query_matches_count = buffer_text
1878            .chars()
1879            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1880            .count();
1881        assert!(
1882            expected_query_matches_count > 1,
1883            "Should pick a query with multiple results"
1884        );
1885        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1886        let window = cx.add_window(|_, _| gpui::Empty);
1887
1888        let editor = window.build_entity(cx, |window, cx| {
1889            Editor::for_buffer(buffer.clone(), None, window, cx)
1890        });
1891
1892        let search_bar = window.build_entity(cx, |window, cx| {
1893            let mut search_bar = BufferSearchBar::new(None, window, cx);
1894            search_bar.set_active_pane_item(Some(&editor), window, cx);
1895            search_bar.show(window, cx);
1896            search_bar
1897        });
1898
1899        window
1900            .update(cx, |_, window, cx| {
1901                search_bar.update(cx, |search_bar, cx| {
1902                    search_bar.search("a", None, window, cx)
1903                })
1904            })
1905            .unwrap()
1906            .await
1907            .unwrap();
1908        let initial_selections = window
1909            .update(cx, |_, window, cx| {
1910                search_bar.update(cx, |search_bar, cx| {
1911                    let handle = search_bar.query_editor.focus_handle(cx);
1912                    window.focus(&handle);
1913                    search_bar.activate_current_match(window, cx);
1914                });
1915                assert!(
1916                    !editor.read(cx).is_focused(window),
1917                    "Initially, the editor should not be focused"
1918                );
1919                let initial_selections = editor.update(cx, |editor, cx| {
1920                    let initial_selections = editor.selections.display_ranges(cx);
1921                    assert_eq!(
1922                        initial_selections.len(), 1,
1923                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1924                    );
1925                    initial_selections
1926                });
1927                search_bar.update(cx, |search_bar, cx| {
1928                    assert_eq!(search_bar.active_match_index, Some(0));
1929                    let handle = search_bar.query_editor.focus_handle(cx);
1930                    window.focus(&handle);
1931                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1932                });
1933                assert!(
1934                    editor.read(cx).is_focused(window),
1935                    "Should focus editor after successful SelectAllMatches"
1936                );
1937                search_bar.update(cx, |search_bar, cx| {
1938                    let all_selections =
1939                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1940                    assert_eq!(
1941                        all_selections.len(),
1942                        expected_query_matches_count,
1943                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1944                    );
1945                    assert_eq!(
1946                        search_bar.active_match_index,
1947                        Some(0),
1948                        "Match index should not change after selecting all matches"
1949                    );
1950                });
1951
1952                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
1953                initial_selections
1954            }).unwrap();
1955
1956        window
1957            .update(cx, |_, window, cx| {
1958                assert!(
1959                    editor.read(cx).is_focused(window),
1960                    "Should still have editor focused after SelectNextMatch"
1961                );
1962                search_bar.update(cx, |search_bar, cx| {
1963                    let all_selections =
1964                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1965                    assert_eq!(
1966                        all_selections.len(),
1967                        1,
1968                        "On next match, should deselect items and select the next match"
1969                    );
1970                    assert_ne!(
1971                        all_selections, initial_selections,
1972                        "Next match should be different from the first selection"
1973                    );
1974                    assert_eq!(
1975                        search_bar.active_match_index,
1976                        Some(1),
1977                        "Match index should be updated to the next one"
1978                    );
1979                    let handle = search_bar.query_editor.focus_handle(cx);
1980                    window.focus(&handle);
1981                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
1982                });
1983            })
1984            .unwrap();
1985        window
1986            .update(cx, |_, window, cx| {
1987                assert!(
1988                    editor.read(cx).is_focused(window),
1989                    "Should focus editor after successful SelectAllMatches"
1990                );
1991                search_bar.update(cx, |search_bar, cx| {
1992                    let all_selections =
1993                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1994                    assert_eq!(
1995                    all_selections.len(),
1996                    expected_query_matches_count,
1997                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1998                );
1999                    assert_eq!(
2000                        search_bar.active_match_index,
2001                        Some(1),
2002                        "Match index should not change after selecting all matches"
2003                    );
2004                });
2005                search_bar.update(cx, |search_bar, cx| {
2006                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2007                });
2008            })
2009            .unwrap();
2010        let last_match_selections = window
2011            .update(cx, |_, window, cx| {
2012                assert!(
2013                    editor.read(cx).is_focused(window),
2014                    "Should still have editor focused after SelectPreviousMatch"
2015                );
2016
2017                search_bar.update(cx, |search_bar, cx| {
2018                    let all_selections =
2019                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2020                    assert_eq!(
2021                        all_selections.len(),
2022                        1,
2023                        "On previous match, should deselect items and select the previous item"
2024                    );
2025                    assert_eq!(
2026                        all_selections, initial_selections,
2027                        "Previous match should be the same as the first selection"
2028                    );
2029                    assert_eq!(
2030                        search_bar.active_match_index,
2031                        Some(0),
2032                        "Match index should be updated to the previous one"
2033                    );
2034                    all_selections
2035                })
2036            })
2037            .unwrap();
2038
2039        window
2040            .update(cx, |_, window, cx| {
2041                search_bar.update(cx, |search_bar, cx| {
2042                    let handle = search_bar.query_editor.focus_handle(cx);
2043                    window.focus(&handle);
2044                    search_bar.search("abas_nonexistent_match", None, window, cx)
2045                })
2046            })
2047            .unwrap()
2048            .await
2049            .unwrap();
2050        window
2051            .update(cx, |_, window, cx| {
2052                search_bar.update(cx, |search_bar, cx| {
2053                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2054                });
2055                assert!(
2056                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2057                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2058                );
2059                search_bar.update(cx, |search_bar, cx| {
2060                    let all_selections =
2061                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2062                    assert_eq!(
2063                        all_selections, last_match_selections,
2064                        "Should not select anything new if there are no matches"
2065                    );
2066                    assert!(
2067                        search_bar.active_match_index.is_none(),
2068                        "For no matches, there should be no active match index"
2069                    );
2070                });
2071            })
2072            .unwrap();
2073    }
2074
2075    #[gpui::test]
2076    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2077        init_globals(cx);
2078        let buffer_text = r#"
2079        self.buffer.update(cx, |buffer, cx| {
2080            buffer.edit(
2081                edits,
2082                Some(AutoindentMode::Block {
2083                    original_indent_columns,
2084                }),
2085                cx,
2086            )
2087        });
2088
2089        this.buffer.update(cx, |buffer, cx| {
2090            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2091        });
2092        "#
2093        .unindent();
2094        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2095        let cx = cx.add_empty_window();
2096
2097        let editor =
2098            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2099
2100        let search_bar = cx.new_window_entity(|window, cx| {
2101            let mut search_bar = BufferSearchBar::new(None, window, cx);
2102            search_bar.set_active_pane_item(Some(&editor), window, cx);
2103            search_bar.show(window, cx);
2104            search_bar
2105        });
2106
2107        search_bar
2108            .update_in(cx, |search_bar, window, cx| {
2109                search_bar.search(
2110                    "edit\\(",
2111                    Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2112                    window,
2113                    cx,
2114                )
2115            })
2116            .await
2117            .unwrap();
2118
2119        search_bar.update_in(cx, |search_bar, window, cx| {
2120            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2121        });
2122        search_bar.update(cx, |_, cx| {
2123            let all_selections =
2124                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2125            assert_eq!(
2126                all_selections.len(),
2127                2,
2128                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2129            );
2130        });
2131
2132        search_bar
2133            .update_in(cx, |search_bar, window, cx| {
2134                search_bar.search(
2135                    "edit(",
2136                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2137                    window,
2138                    cx,
2139                )
2140            })
2141            .await
2142            .unwrap();
2143
2144        search_bar.update_in(cx, |search_bar, window, cx| {
2145            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2146        });
2147        search_bar.update(cx, |_, cx| {
2148            let all_selections =
2149                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
2150            assert_eq!(
2151                all_selections.len(),
2152                2,
2153                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2154            );
2155        });
2156    }
2157
2158    #[gpui::test]
2159    async fn test_search_query_history(cx: &mut TestAppContext) {
2160        init_globals(cx);
2161        let buffer_text = r#"
2162        A regular expression (shortened as regex or regexp;[1] also referred to as
2163        rational expression[2][3]) is a sequence of characters that specifies a search
2164        pattern in text. Usually such patterns are used by string-searching algorithms
2165        for "find" or "find and replace" operations on strings, or for input validation.
2166        "#
2167        .unindent();
2168        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2169        let cx = cx.add_empty_window();
2170
2171        let editor =
2172            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2173
2174        let search_bar = cx.new_window_entity(|window, cx| {
2175            let mut search_bar = BufferSearchBar::new(None, window, cx);
2176            search_bar.set_active_pane_item(Some(&editor), window, cx);
2177            search_bar.show(window, cx);
2178            search_bar
2179        });
2180
2181        // Add 3 search items into the history.
2182        search_bar
2183            .update_in(cx, |search_bar, window, cx| {
2184                search_bar.search("a", None, window, cx)
2185            })
2186            .await
2187            .unwrap();
2188        search_bar
2189            .update_in(cx, |search_bar, window, cx| {
2190                search_bar.search("b", None, window, cx)
2191            })
2192            .await
2193            .unwrap();
2194        search_bar
2195            .update_in(cx, |search_bar, window, cx| {
2196                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), window, cx)
2197            })
2198            .await
2199            .unwrap();
2200        // Ensure that the latest search is active.
2201        search_bar.update(cx, |search_bar, cx| {
2202            assert_eq!(search_bar.query(cx), "c");
2203            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2204        });
2205
2206        // Next history query after the latest should set the query to the empty string.
2207        search_bar.update_in(cx, |search_bar, window, cx| {
2208            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2209        });
2210        search_bar.update(cx, |search_bar, cx| {
2211            assert_eq!(search_bar.query(cx), "");
2212            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2213        });
2214        search_bar.update_in(cx, |search_bar, window, cx| {
2215            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2216        });
2217        search_bar.update(cx, |search_bar, cx| {
2218            assert_eq!(search_bar.query(cx), "");
2219            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2220        });
2221
2222        // First previous query for empty current query should set the query to the latest.
2223        search_bar.update_in(cx, |search_bar, window, cx| {
2224            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2225        });
2226        search_bar.update(cx, |search_bar, cx| {
2227            assert_eq!(search_bar.query(cx), "c");
2228            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2229        });
2230
2231        // Further previous items should go over the history in reverse order.
2232        search_bar.update_in(cx, |search_bar, window, cx| {
2233            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2234        });
2235        search_bar.update(cx, |search_bar, cx| {
2236            assert_eq!(search_bar.query(cx), "b");
2237            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2238        });
2239
2240        // Previous items should never go behind the first history item.
2241        search_bar.update_in(cx, |search_bar, window, cx| {
2242            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2243        });
2244        search_bar.update(cx, |search_bar, cx| {
2245            assert_eq!(search_bar.query(cx), "a");
2246            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2247        });
2248        search_bar.update_in(cx, |search_bar, window, cx| {
2249            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2250        });
2251        search_bar.update(cx, |search_bar, cx| {
2252            assert_eq!(search_bar.query(cx), "a");
2253            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2254        });
2255
2256        // Next items should go over the history in the original order.
2257        search_bar.update_in(cx, |search_bar, window, cx| {
2258            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2259        });
2260        search_bar.update(cx, |search_bar, cx| {
2261            assert_eq!(search_bar.query(cx), "b");
2262            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2263        });
2264
2265        search_bar
2266            .update_in(cx, |search_bar, window, cx| {
2267                search_bar.search("ba", None, window, cx)
2268            })
2269            .await
2270            .unwrap();
2271        search_bar.update(cx, |search_bar, cx| {
2272            assert_eq!(search_bar.query(cx), "ba");
2273            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2274        });
2275
2276        // New search input should add another entry to history and move the selection to the end of the history.
2277        search_bar.update_in(cx, |search_bar, window, cx| {
2278            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2279        });
2280        search_bar.update(cx, |search_bar, cx| {
2281            assert_eq!(search_bar.query(cx), "c");
2282            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2283        });
2284        search_bar.update_in(cx, |search_bar, window, cx| {
2285            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2286        });
2287        search_bar.update(cx, |search_bar, cx| {
2288            assert_eq!(search_bar.query(cx), "b");
2289            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2290        });
2291        search_bar.update_in(cx, |search_bar, window, cx| {
2292            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2293        });
2294        search_bar.update(cx, |search_bar, cx| {
2295            assert_eq!(search_bar.query(cx), "c");
2296            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2297        });
2298        search_bar.update_in(cx, |search_bar, window, cx| {
2299            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2300        });
2301        search_bar.update(cx, |search_bar, cx| {
2302            assert_eq!(search_bar.query(cx), "ba");
2303            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2304        });
2305        search_bar.update_in(cx, |search_bar, window, cx| {
2306            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2307        });
2308        search_bar.update(cx, |search_bar, cx| {
2309            assert_eq!(search_bar.query(cx), "");
2310            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2311        });
2312    }
2313
2314    #[gpui::test]
2315    async fn test_replace_simple(cx: &mut TestAppContext) {
2316        let (editor, search_bar, cx) = init_test(cx);
2317
2318        search_bar
2319            .update_in(cx, |search_bar, window, cx| {
2320                search_bar.search("expression", None, window, cx)
2321            })
2322            .await
2323            .unwrap();
2324
2325        search_bar.update_in(cx, |search_bar, window, cx| {
2326            search_bar.replacement_editor.update(cx, |editor, cx| {
2327                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2328                editor.set_text("expr$1", window, cx);
2329            });
2330            search_bar.replace_all(&ReplaceAll, window, cx)
2331        });
2332        assert_eq!(
2333            editor.read_with(cx, |this, cx| { this.text(cx) }),
2334            r#"
2335        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2336        rational expr$1[2][3]) is a sequence of characters that specifies a search
2337        pattern in text. Usually such patterns are used by string-searching algorithms
2338        for "find" or "find and replace" operations on strings, or for input validation.
2339        "#
2340            .unindent()
2341        );
2342
2343        // Search for word boundaries and replace just a single one.
2344        search_bar
2345            .update_in(cx, |search_bar, window, cx| {
2346                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), window, cx)
2347            })
2348            .await
2349            .unwrap();
2350
2351        search_bar.update_in(cx, |search_bar, window, cx| {
2352            search_bar.replacement_editor.update(cx, |editor, cx| {
2353                editor.set_text("banana", window, cx);
2354            });
2355            search_bar.replace_next(&ReplaceNext, window, cx)
2356        });
2357        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2358        assert_eq!(
2359            editor.read_with(cx, |this, cx| { this.text(cx) }),
2360            r#"
2361        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2362        rational expr$1[2][3]) is a sequence of characters that specifies a search
2363        pattern in text. Usually such patterns are used by string-searching algorithms
2364        for "find" or "find and replace" operations on strings, or for input validation.
2365        "#
2366            .unindent()
2367        );
2368        // Let's turn on regex mode.
2369        search_bar
2370            .update_in(cx, |search_bar, window, cx| {
2371                search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), window, cx)
2372            })
2373            .await
2374            .unwrap();
2375        search_bar.update_in(cx, |search_bar, window, cx| {
2376            search_bar.replacement_editor.update(cx, |editor, cx| {
2377                editor.set_text("${1}number", window, cx);
2378            });
2379            search_bar.replace_all(&ReplaceAll, window, cx)
2380        });
2381        assert_eq!(
2382            editor.read_with(cx, |this, cx| { this.text(cx) }),
2383            r#"
2384        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2385        rational expr$12number3number) is a sequence of characters that specifies a search
2386        pattern in text. Usually such patterns are used by string-searching algorithms
2387        for "find" or "find and replace" operations on strings, or for input validation.
2388        "#
2389            .unindent()
2390        );
2391        // Now with a whole-word twist.
2392        search_bar
2393            .update_in(cx, |search_bar, window, cx| {
2394                search_bar.search(
2395                    "a\\w+s",
2396                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2397                    window,
2398                    cx,
2399                )
2400            })
2401            .await
2402            .unwrap();
2403        search_bar.update_in(cx, |search_bar, window, cx| {
2404            search_bar.replacement_editor.update(cx, |editor, cx| {
2405                editor.set_text("things", window, cx);
2406            });
2407            search_bar.replace_all(&ReplaceAll, window, cx)
2408        });
2409        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2410        // of words in this text that would match this regex if not for WHOLE_WORD.
2411        assert_eq!(
2412            editor.read_with(cx, |this, cx| { this.text(cx) }),
2413            r#"
2414        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2415        rational expr$12number3number) is a sequence of characters that specifies a search
2416        pattern in text. Usually such patterns are used by string-searching things
2417        for "find" or "find and replace" operations on strings, or for input validation.
2418        "#
2419            .unindent()
2420        );
2421    }
2422
2423    struct ReplacementTestParams<'a> {
2424        editor: &'a Entity<Editor>,
2425        search_bar: &'a Entity<BufferSearchBar>,
2426        cx: &'a mut VisualTestContext,
2427        search_text: &'static str,
2428        search_options: Option<SearchOptions>,
2429        replacement_text: &'static str,
2430        replace_all: bool,
2431        expected_text: String,
2432    }
2433
2434    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2435        options
2436            .search_bar
2437            .update_in(options.cx, |search_bar, window, cx| {
2438                if let Some(options) = options.search_options {
2439                    search_bar.set_search_options(options, cx);
2440                }
2441                search_bar.search(options.search_text, options.search_options, window, cx)
2442            })
2443            .await
2444            .unwrap();
2445
2446        options
2447            .search_bar
2448            .update_in(options.cx, |search_bar, window, cx| {
2449                search_bar.replacement_editor.update(cx, |editor, cx| {
2450                    editor.set_text(options.replacement_text, window, cx);
2451                });
2452
2453                if options.replace_all {
2454                    search_bar.replace_all(&ReplaceAll, window, cx)
2455                } else {
2456                    search_bar.replace_next(&ReplaceNext, window, cx)
2457                }
2458            });
2459
2460        assert_eq!(
2461            options
2462                .editor
2463                .read_with(options.cx, |this, cx| { this.text(cx) }),
2464            options.expected_text
2465        );
2466    }
2467
2468    #[gpui::test]
2469    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2470        let (editor, search_bar, cx) = init_test(cx);
2471
2472        run_replacement_test(ReplacementTestParams {
2473            editor: &editor,
2474            search_bar: &search_bar,
2475            cx,
2476            search_text: "expression",
2477            search_options: None,
2478            replacement_text: r"\n",
2479            replace_all: true,
2480            expected_text: r#"
2481            A regular \n (shortened as regex or regexp;[1] also referred to as
2482            rational \n[2][3]) is a sequence of characters that specifies a search
2483            pattern in text. Usually such patterns are used by string-searching algorithms
2484            for "find" or "find and replace" operations on strings, or for input validation.
2485            "#
2486            .unindent(),
2487        })
2488        .await;
2489
2490        run_replacement_test(ReplacementTestParams {
2491            editor: &editor,
2492            search_bar: &search_bar,
2493            cx,
2494            search_text: "or",
2495            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2496            replacement_text: r"\\\n\\\\",
2497            replace_all: false,
2498            expected_text: r#"
2499            A regular \n (shortened as regex \
2500            \\ regexp;[1] also referred to as
2501            rational \n[2][3]) is a sequence of characters that specifies a search
2502            pattern in text. Usually such patterns are used by string-searching algorithms
2503            for "find" or "find and replace" operations on strings, or for input validation.
2504            "#
2505            .unindent(),
2506        })
2507        .await;
2508
2509        run_replacement_test(ReplacementTestParams {
2510            editor: &editor,
2511            search_bar: &search_bar,
2512            cx,
2513            search_text: r"(that|used) ",
2514            search_options: Some(SearchOptions::REGEX),
2515            replacement_text: r"$1\n",
2516            replace_all: true,
2517            expected_text: r#"
2518            A regular \n (shortened as regex \
2519            \\ regexp;[1] also referred to as
2520            rational \n[2][3]) is a sequence of characters that
2521            specifies a search
2522            pattern in text. Usually such patterns are used
2523            by string-searching algorithms
2524            for "find" or "find and replace" operations on strings, or for input validation.
2525            "#
2526            .unindent(),
2527        })
2528        .await;
2529    }
2530
2531    #[gpui::test]
2532    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2533        cx: &mut TestAppContext,
2534    ) {
2535        init_globals(cx);
2536        let buffer = cx.new(|cx| {
2537            Buffer::local(
2538                r#"
2539                aaa bbb aaa ccc
2540                aaa bbb aaa ccc
2541                aaa bbb aaa ccc
2542                aaa bbb aaa ccc
2543                aaa bbb aaa ccc
2544                aaa bbb aaa ccc
2545                "#
2546                .unindent(),
2547                cx,
2548            )
2549        });
2550        let cx = cx.add_empty_window();
2551        let editor =
2552            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2553
2554        let search_bar = cx.new_window_entity(|window, cx| {
2555            let mut search_bar = BufferSearchBar::new(None, window, cx);
2556            search_bar.set_active_pane_item(Some(&editor), window, cx);
2557            search_bar.show(window, cx);
2558            search_bar
2559        });
2560
2561        editor.update_in(cx, |editor, window, cx| {
2562            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2563                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2564            })
2565        });
2566
2567        search_bar.update_in(cx, |search_bar, window, cx| {
2568            let deploy = Deploy {
2569                focus: true,
2570                replace_enabled: false,
2571                selection_search_enabled: true,
2572            };
2573            search_bar.deploy(&deploy, window, cx);
2574        });
2575
2576        cx.run_until_parked();
2577
2578        search_bar
2579            .update_in(cx, |search_bar, window, cx| {
2580                search_bar.search("aaa", None, window, cx)
2581            })
2582            .await
2583            .unwrap();
2584
2585        editor.update(cx, |editor, cx| {
2586            assert_eq!(
2587                editor.search_background_highlights(cx),
2588                &[
2589                    Point::new(1, 0)..Point::new(1, 3),
2590                    Point::new(1, 8)..Point::new(1, 11),
2591                    Point::new(2, 0)..Point::new(2, 3),
2592                ]
2593            );
2594        });
2595    }
2596
2597    #[gpui::test]
2598    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2599        cx: &mut TestAppContext,
2600    ) {
2601        init_globals(cx);
2602        let text = r#"
2603            aaa bbb aaa ccc
2604            aaa bbb aaa ccc
2605            aaa bbb aaa ccc
2606            aaa bbb aaa ccc
2607            aaa bbb aaa ccc
2608            aaa bbb aaa ccc
2609
2610            aaa bbb aaa ccc
2611            aaa bbb aaa ccc
2612            aaa bbb aaa ccc
2613            aaa bbb aaa ccc
2614            aaa bbb aaa ccc
2615            aaa bbb aaa ccc
2616            "#
2617        .unindent();
2618
2619        let cx = cx.add_empty_window();
2620        let editor = cx.new_window_entity(|window, cx| {
2621            let multibuffer = MultiBuffer::build_multi(
2622                [
2623                    (
2624                        &text,
2625                        vec![
2626                            Point::new(0, 0)..Point::new(2, 0),
2627                            Point::new(4, 0)..Point::new(5, 0),
2628                        ],
2629                    ),
2630                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2631                ],
2632                cx,
2633            );
2634            Editor::for_multibuffer(multibuffer, None, window, cx)
2635        });
2636
2637        let search_bar = cx.new_window_entity(|window, cx| {
2638            let mut search_bar = BufferSearchBar::new(None, window, cx);
2639            search_bar.set_active_pane_item(Some(&editor), window, cx);
2640            search_bar.show(window, cx);
2641            search_bar
2642        });
2643
2644        editor.update_in(cx, |editor, window, cx| {
2645            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2646                s.select_ranges(vec![
2647                    Point::new(1, 0)..Point::new(1, 4),
2648                    Point::new(5, 3)..Point::new(6, 4),
2649                ])
2650            })
2651        });
2652
2653        search_bar.update_in(cx, |search_bar, window, cx| {
2654            let deploy = Deploy {
2655                focus: true,
2656                replace_enabled: false,
2657                selection_search_enabled: true,
2658            };
2659            search_bar.deploy(&deploy, window, cx);
2660        });
2661
2662        cx.run_until_parked();
2663
2664        search_bar
2665            .update_in(cx, |search_bar, window, cx| {
2666                search_bar.search("aaa", None, window, cx)
2667            })
2668            .await
2669            .unwrap();
2670
2671        editor.update(cx, |editor, cx| {
2672            assert_eq!(
2673                editor.search_background_highlights(cx),
2674                &[
2675                    Point::new(1, 0)..Point::new(1, 3),
2676                    Point::new(5, 8)..Point::new(5, 11),
2677                    Point::new(6, 0)..Point::new(6, 3),
2678                ]
2679            );
2680        });
2681    }
2682
2683    #[gpui::test]
2684    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2685        let (editor, search_bar, cx) = init_test(cx);
2686        // Search using valid regexp
2687        search_bar
2688            .update_in(cx, |search_bar, window, cx| {
2689                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2690                search_bar.search("expression", None, window, cx)
2691            })
2692            .await
2693            .unwrap();
2694        editor.update_in(cx, |editor, window, cx| {
2695            assert_eq!(
2696                display_points_of(editor.all_text_background_highlights(window, cx)),
2697                &[
2698                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2699                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2700                ],
2701            );
2702        });
2703
2704        // Now, the expression is invalid
2705        search_bar
2706            .update_in(cx, |search_bar, window, cx| {
2707                search_bar.search("expression (", None, window, cx)
2708            })
2709            .await
2710            .unwrap_err();
2711        editor.update_in(cx, |editor, window, cx| {
2712            assert!(
2713                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2714            );
2715        });
2716    }
2717
2718    #[gpui::test]
2719    async fn test_search_options_changes(cx: &mut TestAppContext) {
2720        let (_editor, search_bar, cx) = init_test(cx);
2721        update_search_settings(
2722            SearchSettings {
2723                button: true,
2724                whole_word: false,
2725                case_sensitive: false,
2726                include_ignored: false,
2727                regex: false,
2728            },
2729            cx,
2730        );
2731
2732        let deploy = Deploy {
2733            focus: true,
2734            replace_enabled: false,
2735            selection_search_enabled: true,
2736        };
2737
2738        search_bar.update_in(cx, |search_bar, window, cx| {
2739            assert_eq!(
2740                search_bar.search_options,
2741                SearchOptions::NONE,
2742                "Should have no search options enabled by default"
2743            );
2744            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2745            assert_eq!(
2746                search_bar.search_options,
2747                SearchOptions::WHOLE_WORD,
2748                "Should enable the option toggled"
2749            );
2750            assert!(
2751                !search_bar.dismissed,
2752                "Search bar should be present and visible"
2753            );
2754            search_bar.deploy(&deploy, window, cx);
2755            assert_eq!(
2756                search_bar.search_options,
2757                SearchOptions::WHOLE_WORD,
2758                "After (re)deploying, the option should still be enabled"
2759            );
2760
2761            search_bar.dismiss(&Dismiss, window, cx);
2762            search_bar.deploy(&deploy, window, cx);
2763            assert_eq!(
2764                search_bar.search_options,
2765                SearchOptions::WHOLE_WORD,
2766                "After hiding and showing the search bar, search options should be preserved"
2767            );
2768
2769            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2770            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2771            assert_eq!(
2772                search_bar.search_options,
2773                SearchOptions::REGEX,
2774                "Should enable the options toggled"
2775            );
2776            assert!(
2777                !search_bar.dismissed,
2778                "Search bar should be present and visible"
2779            );
2780            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2781        });
2782
2783        update_search_settings(
2784            SearchSettings {
2785                button: true,
2786                whole_word: false,
2787                case_sensitive: true,
2788                include_ignored: false,
2789                regex: false,
2790            },
2791            cx,
2792        );
2793        search_bar.update_in(cx, |search_bar, window, cx| {
2794            assert_eq!(
2795                search_bar.search_options,
2796                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2797                "Should have no search options enabled by default"
2798            );
2799
2800            search_bar.deploy(&deploy, window, cx);
2801            assert_eq!(
2802                search_bar.search_options,
2803                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2804                "Toggling a non-dismissed search bar with custom options should not change the default options"
2805            );
2806            search_bar.dismiss(&Dismiss, window, cx);
2807            search_bar.deploy(&deploy, window, cx);
2808            assert_eq!(
2809                search_bar.configured_options,
2810                SearchOptions::CASE_SENSITIVE,
2811                "After a settings update and toggling the search bar, configured options should be updated"
2812            );
2813            assert_eq!(
2814                search_bar.search_options,
2815                SearchOptions::CASE_SENSITIVE,
2816                "After a settings update and toggling the search bar, configured options should be used"
2817            );
2818        });
2819
2820        update_search_settings(
2821            SearchSettings {
2822                button: true,
2823                whole_word: true,
2824                case_sensitive: true,
2825                include_ignored: false,
2826                regex: false,
2827            },
2828            cx,
2829        );
2830
2831        search_bar.update_in(cx, |search_bar, window, cx| {
2832            search_bar.deploy(&deploy, window, cx);
2833            search_bar.dismiss(&Dismiss, window, cx);
2834            search_bar.show(window, cx);
2835            assert_eq!(
2836                search_bar.search_options,
2837                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2838                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2839            );
2840        });
2841    }
2842
2843    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2844        cx.update(|cx| {
2845            SettingsStore::update_global(cx, |store, cx| {
2846                store.update_user_settings::<EditorSettings>(cx, |settings| {
2847                    settings.search = Some(search_settings);
2848                });
2849            });
2850        });
2851    }
2852}