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