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