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