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