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 settings::{SearchSettingsContent, SettingsStore};
1536    use smol::stream::StreamExt as _;
1537    use unindent::Unindent as _;
1538    use util_macros::perf;
1539
1540    fn init_globals(cx: &mut TestAppContext) {
1541        cx.update(|cx| {
1542            let store = settings::SettingsStore::test(cx);
1543            cx.set_global(store);
1544            editor::init(cx);
1545
1546            theme::init(theme::LoadThemes::JustBase, cx);
1547            crate::init(cx);
1548        });
1549    }
1550
1551    fn init_test(
1552        cx: &mut TestAppContext,
1553    ) -> (
1554        Entity<Editor>,
1555        Entity<BufferSearchBar>,
1556        &mut VisualTestContext,
1557    ) {
1558        init_globals(cx);
1559        let buffer = cx.new(|cx| {
1560            Buffer::local(
1561                r#"
1562                A regular expression (shortened as regex or regexp;[1] also referred to as
1563                rational expression[2][3]) is a sequence of characters that specifies a search
1564                pattern in text. Usually such patterns are used by string-searching algorithms
1565                for "find" or "find and replace" operations on strings, or for input validation.
1566                "#
1567                .unindent(),
1568                cx,
1569            )
1570        });
1571        let mut editor = None;
1572        let window = cx.add_window(|window, cx| {
1573            let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
1574                "keymaps/default-macos.json",
1575                cx,
1576            )
1577            .unwrap();
1578            cx.bind_keys(default_key_bindings);
1579            editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
1580            let mut search_bar = BufferSearchBar::new(None, window, cx);
1581            search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
1582            search_bar.show(window, cx);
1583            search_bar
1584        });
1585        let search_bar = window.root(cx).unwrap();
1586
1587        let cx = VisualTestContext::from_window(*window, cx).into_mut();
1588
1589        (editor.unwrap(), search_bar, cx)
1590    }
1591
1592    #[perf]
1593    #[gpui::test]
1594    async fn test_search_simple(cx: &mut TestAppContext) {
1595        let (editor, search_bar, cx) = init_test(cx);
1596        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1597            background_highlights
1598                .into_iter()
1599                .map(|(range, _)| range)
1600                .collect::<Vec<_>>()
1601        };
1602        // Search for a string that appears with different casing.
1603        // By default, search is case-insensitive.
1604        search_bar
1605            .update_in(cx, |search_bar, window, cx| {
1606                search_bar.search("us", None, true, window, cx)
1607            })
1608            .await
1609            .unwrap();
1610        editor.update_in(cx, |editor, window, cx| {
1611            assert_eq!(
1612                display_points_of(editor.all_text_background_highlights(window, cx)),
1613                &[
1614                    DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
1615                    DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
1616                ]
1617            );
1618        });
1619
1620        // Switch to a case sensitive search.
1621        search_bar.update_in(cx, |search_bar, window, cx| {
1622            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
1623        });
1624        let mut editor_notifications = cx.notifications(&editor);
1625        editor_notifications.next().await;
1626        editor.update_in(cx, |editor, window, cx| {
1627            assert_eq!(
1628                display_points_of(editor.all_text_background_highlights(window, cx)),
1629                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1630            );
1631        });
1632
1633        // Search for a string that appears both as a whole word and
1634        // within other words. By default, all results are found.
1635        search_bar
1636            .update_in(cx, |search_bar, window, cx| {
1637                search_bar.search("or", None, true, window, cx)
1638            })
1639            .await
1640            .unwrap();
1641        editor.update_in(cx, |editor, window, cx| {
1642            assert_eq!(
1643                display_points_of(editor.all_text_background_highlights(window, cx)),
1644                &[
1645                    DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
1646                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1647                    DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
1648                    DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
1649                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1650                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1651                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
1652                ]
1653            );
1654        });
1655
1656        // Switch to a whole word search.
1657        search_bar.update_in(cx, |search_bar, window, cx| {
1658            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
1659        });
1660        let mut editor_notifications = cx.notifications(&editor);
1661        editor_notifications.next().await;
1662        editor.update_in(cx, |editor, window, cx| {
1663            assert_eq!(
1664                display_points_of(editor.all_text_background_highlights(window, cx)),
1665                &[
1666                    DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
1667                    DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
1668                    DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
1669                ]
1670            );
1671        });
1672
1673        editor.update_in(cx, |editor, window, cx| {
1674            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1675                s.select_display_ranges([
1676                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1677                ])
1678            });
1679        });
1680        search_bar.update_in(cx, |search_bar, window, cx| {
1681            assert_eq!(search_bar.active_match_index, Some(0));
1682            search_bar.select_next_match(&SelectNextMatch, window, cx);
1683            assert_eq!(
1684                editor.update(cx, |editor, cx| editor
1685                    .selections
1686                    .display_ranges(&editor.display_snapshot(cx))),
1687                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1688            );
1689        });
1690        search_bar.read_with(cx, |search_bar, _| {
1691            assert_eq!(search_bar.active_match_index, Some(0));
1692        });
1693
1694        search_bar.update_in(cx, |search_bar, window, cx| {
1695            search_bar.select_next_match(&SelectNextMatch, window, cx);
1696            assert_eq!(
1697                editor.update(cx, |editor, cx| editor
1698                    .selections
1699                    .display_ranges(&editor.display_snapshot(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
1711                    .selections
1712                    .display_ranges(&editor.display_snapshot(cx))),
1713                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1714            );
1715        });
1716        search_bar.read_with(cx, |search_bar, _| {
1717            assert_eq!(search_bar.active_match_index, Some(2));
1718        });
1719
1720        search_bar.update_in(cx, |search_bar, window, cx| {
1721            search_bar.select_next_match(&SelectNextMatch, window, cx);
1722            assert_eq!(
1723                editor.update(cx, |editor, cx| editor
1724                    .selections
1725                    .display_ranges(&editor.display_snapshot(cx))),
1726                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1727            );
1728        });
1729        search_bar.read_with(cx, |search_bar, _| {
1730            assert_eq!(search_bar.active_match_index, Some(0));
1731        });
1732
1733        search_bar.update_in(cx, |search_bar, window, cx| {
1734            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1735            assert_eq!(
1736                editor.update(cx, |editor, cx| editor
1737                    .selections
1738                    .display_ranges(&editor.display_snapshot(cx))),
1739                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1740            );
1741        });
1742        search_bar.read_with(cx, |search_bar, _| {
1743            assert_eq!(search_bar.active_match_index, Some(2));
1744        });
1745
1746        search_bar.update_in(cx, |search_bar, window, cx| {
1747            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1748            assert_eq!(
1749                editor.update(cx, |editor, cx| editor
1750                    .selections
1751                    .display_ranges(&editor.display_snapshot(cx))),
1752                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1753            );
1754        });
1755        search_bar.read_with(cx, |search_bar, _| {
1756            assert_eq!(search_bar.active_match_index, Some(1));
1757        });
1758
1759        search_bar.update_in(cx, |search_bar, window, cx| {
1760            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1761            assert_eq!(
1762                editor.update(cx, |editor, cx| editor
1763                    .selections
1764                    .display_ranges(&editor.display_snapshot(cx))),
1765                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1766            );
1767        });
1768        search_bar.read_with(cx, |search_bar, _| {
1769            assert_eq!(search_bar.active_match_index, Some(0));
1770        });
1771
1772        // Park the cursor in between matches and ensure that going to the previous match selects
1773        // the closest match to the left.
1774        editor.update_in(cx, |editor, window, cx| {
1775            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1776                s.select_display_ranges([
1777                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1778                ])
1779            });
1780        });
1781        search_bar.update_in(cx, |search_bar, window, cx| {
1782            assert_eq!(search_bar.active_match_index, Some(1));
1783            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1784            assert_eq!(
1785                editor.update(cx, |editor, cx| editor
1786                    .selections
1787                    .display_ranges(&editor.display_snapshot(cx))),
1788                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1789            );
1790        });
1791        search_bar.read_with(cx, |search_bar, _| {
1792            assert_eq!(search_bar.active_match_index, Some(0));
1793        });
1794
1795        // Park the cursor in between matches and ensure that going to the next match selects the
1796        // closest match to the right.
1797        editor.update_in(cx, |editor, window, cx| {
1798            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1799                s.select_display_ranges([
1800                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
1801                ])
1802            });
1803        });
1804        search_bar.update_in(cx, |search_bar, window, cx| {
1805            assert_eq!(search_bar.active_match_index, Some(1));
1806            search_bar.select_next_match(&SelectNextMatch, window, cx);
1807            assert_eq!(
1808                editor.update(cx, |editor, cx| editor
1809                    .selections
1810                    .display_ranges(&editor.display_snapshot(cx))),
1811                [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
1812            );
1813        });
1814        search_bar.read_with(cx, |search_bar, _| {
1815            assert_eq!(search_bar.active_match_index, Some(1));
1816        });
1817
1818        // Park the cursor after the last match and ensure that going to the previous match selects
1819        // the last match.
1820        editor.update_in(cx, |editor, window, cx| {
1821            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1822                s.select_display_ranges([
1823                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1824                ])
1825            });
1826        });
1827        search_bar.update_in(cx, |search_bar, window, cx| {
1828            assert_eq!(search_bar.active_match_index, Some(2));
1829            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1830            assert_eq!(
1831                editor.update(cx, |editor, cx| editor
1832                    .selections
1833                    .display_ranges(&editor.display_snapshot(cx))),
1834                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1835            );
1836        });
1837        search_bar.read_with(cx, |search_bar, _| {
1838            assert_eq!(search_bar.active_match_index, Some(2));
1839        });
1840
1841        // Park the cursor after the last match and ensure that going to the next match selects the
1842        // first match.
1843        editor.update_in(cx, |editor, window, cx| {
1844            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1845                s.select_display_ranges([
1846                    DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
1847                ])
1848            });
1849        });
1850        search_bar.update_in(cx, |search_bar, window, cx| {
1851            assert_eq!(search_bar.active_match_index, Some(2));
1852            search_bar.select_next_match(&SelectNextMatch, window, cx);
1853            assert_eq!(
1854                editor.update(cx, |editor, cx| editor
1855                    .selections
1856                    .display_ranges(&editor.display_snapshot(cx))),
1857                [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
1858            );
1859        });
1860        search_bar.read_with(cx, |search_bar, _| {
1861            assert_eq!(search_bar.active_match_index, Some(0));
1862        });
1863
1864        // Park the cursor before the first match and ensure that going to the previous match
1865        // selects the last match.
1866        editor.update_in(cx, |editor, window, cx| {
1867            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1868                s.select_display_ranges([
1869                    DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
1870                ])
1871            });
1872        });
1873        search_bar.update_in(cx, |search_bar, window, cx| {
1874            assert_eq!(search_bar.active_match_index, Some(0));
1875            search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
1876            assert_eq!(
1877                editor.update(cx, |editor, cx| editor
1878                    .selections
1879                    .display_ranges(&editor.display_snapshot(cx))),
1880                [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
1881            );
1882        });
1883        search_bar.read_with(cx, |search_bar, _| {
1884            assert_eq!(search_bar.active_match_index, Some(2));
1885        });
1886    }
1887
1888    fn display_points_of(
1889        background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
1890    ) -> Vec<Range<DisplayPoint>> {
1891        background_highlights
1892            .into_iter()
1893            .map(|(range, _)| range)
1894            .collect::<Vec<_>>()
1895    }
1896
1897    #[perf]
1898    #[gpui::test]
1899    async fn test_search_option_handling(cx: &mut TestAppContext) {
1900        let (editor, search_bar, cx) = init_test(cx);
1901
1902        // show with options should make current search case sensitive
1903        search_bar
1904            .update_in(cx, |search_bar, window, cx| {
1905                search_bar.show(window, cx);
1906                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
1907            })
1908            .await
1909            .unwrap();
1910        editor.update_in(cx, |editor, window, cx| {
1911            assert_eq!(
1912                display_points_of(editor.all_text_background_highlights(window, cx)),
1913                &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
1914            );
1915        });
1916
1917        // search_suggested should restore default options
1918        search_bar.update_in(cx, |search_bar, window, cx| {
1919            search_bar.search_suggested(window, cx);
1920            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1921        });
1922
1923        // toggling a search option should update the defaults
1924        search_bar
1925            .update_in(cx, |search_bar, window, cx| {
1926                search_bar.search(
1927                    "regex",
1928                    Some(SearchOptions::CASE_SENSITIVE),
1929                    true,
1930                    window,
1931                    cx,
1932                )
1933            })
1934            .await
1935            .unwrap();
1936        search_bar.update_in(cx, |search_bar, window, cx| {
1937            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
1938        });
1939        let mut editor_notifications = cx.notifications(&editor);
1940        editor_notifications.next().await;
1941        editor.update_in(cx, |editor, window, cx| {
1942            assert_eq!(
1943                display_points_of(editor.all_text_background_highlights(window, cx)),
1944                &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
1945            );
1946        });
1947
1948        // defaults should still include whole word
1949        search_bar.update_in(cx, |search_bar, window, cx| {
1950            search_bar.search_suggested(window, cx);
1951            assert_eq!(
1952                search_bar.search_options,
1953                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1954            )
1955        });
1956    }
1957
1958    #[perf]
1959    #[gpui::test]
1960    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1961        init_globals(cx);
1962        let buffer_text = r#"
1963        A regular expression (shortened as regex or regexp;[1] also referred to as
1964        rational expression[2][3]) is a sequence of characters that specifies a search
1965        pattern in text. Usually such patterns are used by string-searching algorithms
1966        for "find" or "find and replace" operations on strings, or for input validation.
1967        "#
1968        .unindent();
1969        let expected_query_matches_count = buffer_text
1970            .chars()
1971            .filter(|c| c.eq_ignore_ascii_case(&'a'))
1972            .count();
1973        assert!(
1974            expected_query_matches_count > 1,
1975            "Should pick a query with multiple results"
1976        );
1977        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
1978        let window = cx.add_window(|_, _| gpui::Empty);
1979
1980        let editor = window.build_entity(cx, |window, cx| {
1981            Editor::for_buffer(buffer.clone(), None, window, cx)
1982        });
1983
1984        let search_bar = window.build_entity(cx, |window, cx| {
1985            let mut search_bar = BufferSearchBar::new(None, window, cx);
1986            search_bar.set_active_pane_item(Some(&editor), window, cx);
1987            search_bar.show(window, cx);
1988            search_bar
1989        });
1990
1991        window
1992            .update(cx, |_, window, cx| {
1993                search_bar.update(cx, |search_bar, cx| {
1994                    search_bar.search("a", None, true, window, cx)
1995                })
1996            })
1997            .unwrap()
1998            .await
1999            .unwrap();
2000        let initial_selections = window
2001            .update(cx, |_, window, cx| {
2002                search_bar.update(cx, |search_bar, cx| {
2003                    let handle = search_bar.query_editor.focus_handle(cx);
2004                    window.focus(&handle);
2005                    search_bar.activate_current_match(window, cx);
2006                });
2007                assert!(
2008                    !editor.read(cx).is_focused(window),
2009                    "Initially, the editor should not be focused"
2010                );
2011                let initial_selections = editor.update(cx, |editor, cx| {
2012                    let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
2013                    assert_eq!(
2014                        initial_selections.len(), 1,
2015                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
2016                    );
2017                    initial_selections
2018                });
2019                search_bar.update(cx, |search_bar, cx| {
2020                    assert_eq!(search_bar.active_match_index, Some(0));
2021                    let handle = search_bar.query_editor.focus_handle(cx);
2022                    window.focus(&handle);
2023                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2024                });
2025                assert!(
2026                    editor.read(cx).is_focused(window),
2027                    "Should focus editor after successful SelectAllMatches"
2028                );
2029                search_bar.update(cx, |search_bar, cx| {
2030                    let all_selections =
2031                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2032                    assert_eq!(
2033                        all_selections.len(),
2034                        expected_query_matches_count,
2035                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2036                    );
2037                    assert_eq!(
2038                        search_bar.active_match_index,
2039                        Some(0),
2040                        "Match index should not change after selecting all matches"
2041                    );
2042                });
2043
2044                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
2045                initial_selections
2046            }).unwrap();
2047
2048        window
2049            .update(cx, |_, window, cx| {
2050                assert!(
2051                    editor.read(cx).is_focused(window),
2052                    "Should still have editor focused after SelectNextMatch"
2053                );
2054                search_bar.update(cx, |search_bar, cx| {
2055                    let all_selections = editor.update(cx, |editor, cx| {
2056                        editor
2057                            .selections
2058                            .display_ranges(&editor.display_snapshot(cx))
2059                    });
2060                    assert_eq!(
2061                        all_selections.len(),
2062                        1,
2063                        "On next match, should deselect items and select the next match"
2064                    );
2065                    assert_ne!(
2066                        all_selections, initial_selections,
2067                        "Next match should be different from the first selection"
2068                    );
2069                    assert_eq!(
2070                        search_bar.active_match_index,
2071                        Some(1),
2072                        "Match index should be updated to the next one"
2073                    );
2074                    let handle = search_bar.query_editor.focus_handle(cx);
2075                    window.focus(&handle);
2076                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2077                });
2078            })
2079            .unwrap();
2080        window
2081            .update(cx, |_, window, cx| {
2082                assert!(
2083                    editor.read(cx).is_focused(window),
2084                    "Should focus editor after successful SelectAllMatches"
2085                );
2086                search_bar.update(cx, |search_bar, cx| {
2087                    let all_selections =
2088                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2089                    assert_eq!(
2090                    all_selections.len(),
2091                    expected_query_matches_count,
2092                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
2093                );
2094                    assert_eq!(
2095                        search_bar.active_match_index,
2096                        Some(1),
2097                        "Match index should not change after selecting all matches"
2098                    );
2099                });
2100                search_bar.update(cx, |search_bar, cx| {
2101                    search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
2102                });
2103            })
2104            .unwrap();
2105        let last_match_selections = window
2106            .update(cx, |_, window, cx| {
2107                assert!(
2108                    editor.read(cx).is_focused(window),
2109                    "Should still have editor focused after SelectPreviousMatch"
2110                );
2111
2112                search_bar.update(cx, |search_bar, cx| {
2113                    let all_selections = editor.update(cx, |editor, cx| {
2114                        editor
2115                            .selections
2116                            .display_ranges(&editor.display_snapshot(cx))
2117                    });
2118                    assert_eq!(
2119                        all_selections.len(),
2120                        1,
2121                        "On previous match, should deselect items and select the previous item"
2122                    );
2123                    assert_eq!(
2124                        all_selections, initial_selections,
2125                        "Previous match should be the same as the first selection"
2126                    );
2127                    assert_eq!(
2128                        search_bar.active_match_index,
2129                        Some(0),
2130                        "Match index should be updated to the previous one"
2131                    );
2132                    all_selections
2133                })
2134            })
2135            .unwrap();
2136
2137        window
2138            .update(cx, |_, window, cx| {
2139                search_bar.update(cx, |search_bar, cx| {
2140                    let handle = search_bar.query_editor.focus_handle(cx);
2141                    window.focus(&handle);
2142                    search_bar.search("abas_nonexistent_match", None, true, window, cx)
2143                })
2144            })
2145            .unwrap()
2146            .await
2147            .unwrap();
2148        window
2149            .update(cx, |_, window, cx| {
2150                search_bar.update(cx, |search_bar, cx| {
2151                    search_bar.select_all_matches(&SelectAllMatches, window, cx);
2152                });
2153                assert!(
2154                    editor.update(cx, |this, _cx| !this.is_focused(window)),
2155                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
2156                );
2157                search_bar.update(cx, |search_bar, cx| {
2158                    let all_selections =
2159                        editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
2160                    assert_eq!(
2161                        all_selections, last_match_selections,
2162                        "Should not select anything new if there are no matches"
2163                    );
2164                    assert!(
2165                        search_bar.active_match_index.is_none(),
2166                        "For no matches, there should be no active match index"
2167                    );
2168                });
2169            })
2170            .unwrap();
2171    }
2172
2173    #[perf]
2174    #[gpui::test]
2175    async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
2176        init_globals(cx);
2177        let buffer_text = r#"
2178        self.buffer.update(cx, |buffer, cx| {
2179            buffer.edit(
2180                edits,
2181                Some(AutoindentMode::Block {
2182                    original_indent_columns,
2183                }),
2184                cx,
2185            )
2186        });
2187
2188        this.buffer.update(cx, |buffer, cx| {
2189            buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
2190        });
2191        "#
2192        .unindent();
2193        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
2194        let cx = cx.add_empty_window();
2195
2196        let editor =
2197            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2198
2199        let search_bar = cx.new_window_entity(|window, cx| {
2200            let mut search_bar = BufferSearchBar::new(None, window, cx);
2201            search_bar.set_active_pane_item(Some(&editor), window, cx);
2202            search_bar.show(window, cx);
2203            search_bar
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::REGEX),
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 = editor.update(cx, |editor, cx| {
2224                editor
2225                    .selections
2226                    .display_ranges(&editor.display_snapshot(cx))
2227            });
2228            assert_eq!(
2229                all_selections.len(),
2230                2,
2231                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2232            );
2233        });
2234
2235        search_bar
2236            .update_in(cx, |search_bar, window, cx| {
2237                search_bar.search(
2238                    "edit(",
2239                    Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
2240                    true,
2241                    window,
2242                    cx,
2243                )
2244            })
2245            .await
2246            .unwrap();
2247
2248        search_bar.update_in(cx, |search_bar, window, cx| {
2249            search_bar.select_all_matches(&SelectAllMatches, window, cx);
2250        });
2251        search_bar.update(cx, |_, cx| {
2252            let all_selections = editor.update(cx, |editor, cx| {
2253                editor
2254                    .selections
2255                    .display_ranges(&editor.display_snapshot(cx))
2256            });
2257            assert_eq!(
2258                all_selections.len(),
2259                2,
2260                "Should select all `edit(` in the buffer, but got: {all_selections:?}"
2261            );
2262        });
2263    }
2264
2265    #[perf]
2266    #[gpui::test]
2267    async fn test_search_query_history(cx: &mut TestAppContext) {
2268        let (_editor, search_bar, cx) = init_test(cx);
2269
2270        // Add 3 search items into the history.
2271        search_bar
2272            .update_in(cx, |search_bar, window, cx| {
2273                search_bar.search("a", None, true, window, cx)
2274            })
2275            .await
2276            .unwrap();
2277        search_bar
2278            .update_in(cx, |search_bar, window, cx| {
2279                search_bar.search("b", None, true, window, cx)
2280            })
2281            .await
2282            .unwrap();
2283        search_bar
2284            .update_in(cx, |search_bar, window, cx| {
2285                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
2286            })
2287            .await
2288            .unwrap();
2289        // Ensure that the latest search is active.
2290        search_bar.update(cx, |search_bar, cx| {
2291            assert_eq!(search_bar.query(cx), "c");
2292            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2293        });
2294
2295        // Next history query after the latest should set the query to the empty string.
2296        search_bar.update_in(cx, |search_bar, window, cx| {
2297            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2298        });
2299        cx.background_executor.run_until_parked();
2300        search_bar.update(cx, |search_bar, cx| {
2301            assert_eq!(search_bar.query(cx), "");
2302            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2303        });
2304        search_bar.update_in(cx, |search_bar, window, cx| {
2305            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2306        });
2307        cx.background_executor.run_until_parked();
2308        search_bar.update(cx, |search_bar, cx| {
2309            assert_eq!(search_bar.query(cx), "");
2310            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2311        });
2312
2313        // First previous query for empty current query should set the query to the latest.
2314        search_bar.update_in(cx, |search_bar, window, cx| {
2315            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2316        });
2317        cx.background_executor.run_until_parked();
2318        search_bar.update(cx, |search_bar, cx| {
2319            assert_eq!(search_bar.query(cx), "c");
2320            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2321        });
2322
2323        // Further previous items should go over the history in reverse order.
2324        search_bar.update_in(cx, |search_bar, window, cx| {
2325            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2326        });
2327        cx.background_executor.run_until_parked();
2328        search_bar.update(cx, |search_bar, cx| {
2329            assert_eq!(search_bar.query(cx), "b");
2330            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2331        });
2332
2333        // Previous items should never go behind the first history item.
2334        search_bar.update_in(cx, |search_bar, window, cx| {
2335            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2336        });
2337        cx.background_executor.run_until_parked();
2338        search_bar.update(cx, |search_bar, cx| {
2339            assert_eq!(search_bar.query(cx), "a");
2340            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2341        });
2342        search_bar.update_in(cx, |search_bar, window, cx| {
2343            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2344        });
2345        cx.background_executor.run_until_parked();
2346        search_bar.update(cx, |search_bar, cx| {
2347            assert_eq!(search_bar.query(cx), "a");
2348            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2349        });
2350
2351        // Next items should go over the history in the original order.
2352        search_bar.update_in(cx, |search_bar, window, cx| {
2353            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2354        });
2355        cx.background_executor.run_until_parked();
2356        search_bar.update(cx, |search_bar, cx| {
2357            assert_eq!(search_bar.query(cx), "b");
2358            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
2359        });
2360
2361        search_bar
2362            .update_in(cx, |search_bar, window, cx| {
2363                search_bar.search("ba", None, true, window, cx)
2364            })
2365            .await
2366            .unwrap();
2367        search_bar.update(cx, |search_bar, cx| {
2368            assert_eq!(search_bar.query(cx), "ba");
2369            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2370        });
2371
2372        // New search input should add another entry to history and move the selection to the end of the history.
2373        search_bar.update_in(cx, |search_bar, window, cx| {
2374            search_bar.previous_history_query(&PreviousHistoryQuery, 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), "c");
2379            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2380        });
2381        search_bar.update_in(cx, |search_bar, window, cx| {
2382            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
2383        });
2384        cx.background_executor.run_until_parked();
2385        search_bar.update(cx, |search_bar, cx| {
2386            assert_eq!(search_bar.query(cx), "b");
2387            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2388        });
2389        search_bar.update_in(cx, |search_bar, window, cx| {
2390            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2391        });
2392        cx.background_executor.run_until_parked();
2393        search_bar.update(cx, |search_bar, cx| {
2394            assert_eq!(search_bar.query(cx), "c");
2395            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2396        });
2397        search_bar.update_in(cx, |search_bar, window, cx| {
2398            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2399        });
2400        cx.background_executor.run_until_parked();
2401        search_bar.update(cx, |search_bar, cx| {
2402            assert_eq!(search_bar.query(cx), "ba");
2403            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2404        });
2405        search_bar.update_in(cx, |search_bar, window, cx| {
2406            search_bar.next_history_query(&NextHistoryQuery, window, cx);
2407        });
2408        cx.background_executor.run_until_parked();
2409        search_bar.update(cx, |search_bar, cx| {
2410            assert_eq!(search_bar.query(cx), "");
2411            assert_eq!(search_bar.search_options, SearchOptions::NONE);
2412        });
2413    }
2414
2415    #[perf]
2416    #[gpui::test]
2417    async fn test_replace_simple(cx: &mut TestAppContext) {
2418        let (editor, search_bar, cx) = init_test(cx);
2419
2420        search_bar
2421            .update_in(cx, |search_bar, window, cx| {
2422                search_bar.search("expression", None, true, window, cx)
2423            })
2424            .await
2425            .unwrap();
2426
2427        search_bar.update_in(cx, |search_bar, window, cx| {
2428            search_bar.replacement_editor.update(cx, |editor, cx| {
2429                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
2430                editor.set_text("expr$1", window, cx);
2431            });
2432            search_bar.replace_all(&ReplaceAll, window, cx)
2433        });
2434        assert_eq!(
2435            editor.read_with(cx, |this, cx| { this.text(cx) }),
2436            r#"
2437        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
2438        rational expr$1[2][3]) is a sequence of characters that specifies a search
2439        pattern in text. Usually such patterns are used by string-searching algorithms
2440        for "find" or "find and replace" operations on strings, or for input validation.
2441        "#
2442            .unindent()
2443        );
2444
2445        // Search for word boundaries and replace just a single one.
2446        search_bar
2447            .update_in(cx, |search_bar, window, cx| {
2448                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
2449            })
2450            .await
2451            .unwrap();
2452
2453        search_bar.update_in(cx, |search_bar, window, cx| {
2454            search_bar.replacement_editor.update(cx, |editor, cx| {
2455                editor.set_text("banana", window, cx);
2456            });
2457            search_bar.replace_next(&ReplaceNext, window, cx)
2458        });
2459        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
2460        assert_eq!(
2461            editor.read_with(cx, |this, cx| { this.text(cx) }),
2462            r#"
2463        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
2464        rational expr$1[2][3]) is a sequence of characters that specifies a search
2465        pattern in text. Usually such patterns are used by string-searching algorithms
2466        for "find" or "find and replace" operations on strings, or for input validation.
2467        "#
2468            .unindent()
2469        );
2470        // Let's turn on regex mode.
2471        search_bar
2472            .update_in(cx, |search_bar, window, cx| {
2473                search_bar.search(
2474                    "\\[([^\\]]+)\\]",
2475                    Some(SearchOptions::REGEX),
2476                    true,
2477                    window,
2478                    cx,
2479                )
2480            })
2481            .await
2482            .unwrap();
2483        search_bar.update_in(cx, |search_bar, window, cx| {
2484            search_bar.replacement_editor.update(cx, |editor, cx| {
2485                editor.set_text("${1}number", window, cx);
2486            });
2487            search_bar.replace_all(&ReplaceAll, window, cx)
2488        });
2489        assert_eq!(
2490            editor.read_with(cx, |this, cx| { this.text(cx) }),
2491            r#"
2492        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2493        rational expr$12number3number) is a sequence of characters that specifies a search
2494        pattern in text. Usually such patterns are used by string-searching algorithms
2495        for "find" or "find and replace" operations on strings, or for input validation.
2496        "#
2497            .unindent()
2498        );
2499        // Now with a whole-word twist.
2500        search_bar
2501            .update_in(cx, |search_bar, window, cx| {
2502                search_bar.search(
2503                    "a\\w+s",
2504                    Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
2505                    true,
2506                    window,
2507                    cx,
2508                )
2509            })
2510            .await
2511            .unwrap();
2512        search_bar.update_in(cx, |search_bar, window, cx| {
2513            search_bar.replacement_editor.update(cx, |editor, cx| {
2514                editor.set_text("things", window, cx);
2515            });
2516            search_bar.replace_all(&ReplaceAll, window, cx)
2517        });
2518        // The only word affected by this edit should be `algorithms`, even though there's a bunch
2519        // of words in this text that would match this regex if not for WHOLE_WORD.
2520        assert_eq!(
2521            editor.read_with(cx, |this, cx| { this.text(cx) }),
2522            r#"
2523        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
2524        rational expr$12number3number) is a sequence of characters that specifies a search
2525        pattern in text. Usually such patterns are used by string-searching things
2526        for "find" or "find and replace" operations on strings, or for input validation.
2527        "#
2528            .unindent()
2529        );
2530    }
2531
2532    struct ReplacementTestParams<'a> {
2533        editor: &'a Entity<Editor>,
2534        search_bar: &'a Entity<BufferSearchBar>,
2535        cx: &'a mut VisualTestContext,
2536        search_text: &'static str,
2537        search_options: Option<SearchOptions>,
2538        replacement_text: &'static str,
2539        replace_all: bool,
2540        expected_text: String,
2541    }
2542
2543    async fn run_replacement_test(options: ReplacementTestParams<'_>) {
2544        options
2545            .search_bar
2546            .update_in(options.cx, |search_bar, window, cx| {
2547                if let Some(options) = options.search_options {
2548                    search_bar.set_search_options(options, cx);
2549                }
2550                search_bar.search(
2551                    options.search_text,
2552                    options.search_options,
2553                    true,
2554                    window,
2555                    cx,
2556                )
2557            })
2558            .await
2559            .unwrap();
2560
2561        options
2562            .search_bar
2563            .update_in(options.cx, |search_bar, window, cx| {
2564                search_bar.replacement_editor.update(cx, |editor, cx| {
2565                    editor.set_text(options.replacement_text, window, cx);
2566                });
2567
2568                if options.replace_all {
2569                    search_bar.replace_all(&ReplaceAll, window, cx)
2570                } else {
2571                    search_bar.replace_next(&ReplaceNext, window, cx)
2572                }
2573            });
2574
2575        assert_eq!(
2576            options
2577                .editor
2578                .read_with(options.cx, |this, cx| { this.text(cx) }),
2579            options.expected_text
2580        );
2581    }
2582
2583    #[perf]
2584    #[gpui::test]
2585    async fn test_replace_special_characters(cx: &mut TestAppContext) {
2586        let (editor, search_bar, cx) = init_test(cx);
2587
2588        run_replacement_test(ReplacementTestParams {
2589            editor: &editor,
2590            search_bar: &search_bar,
2591            cx,
2592            search_text: "expression",
2593            search_options: None,
2594            replacement_text: r"\n",
2595            replace_all: true,
2596            expected_text: r#"
2597            A regular \n (shortened as regex or regexp;[1] also referred to as
2598            rational \n[2][3]) is a sequence of characters that specifies a search
2599            pattern in text. Usually such patterns are used by string-searching algorithms
2600            for "find" or "find and replace" operations on strings, or for input validation.
2601            "#
2602            .unindent(),
2603        })
2604        .await;
2605
2606        run_replacement_test(ReplacementTestParams {
2607            editor: &editor,
2608            search_bar: &search_bar,
2609            cx,
2610            search_text: "or",
2611            search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
2612            replacement_text: r"\\\n\\\\",
2613            replace_all: false,
2614            expected_text: r#"
2615            A regular \n (shortened as regex \
2616            \\ regexp;[1] also referred to as
2617            rational \n[2][3]) is a sequence of characters that specifies a search
2618            pattern in text. Usually such patterns are used by string-searching algorithms
2619            for "find" or "find and replace" operations on strings, or for input validation.
2620            "#
2621            .unindent(),
2622        })
2623        .await;
2624
2625        run_replacement_test(ReplacementTestParams {
2626            editor: &editor,
2627            search_bar: &search_bar,
2628            cx,
2629            search_text: r"(that|used) ",
2630            search_options: Some(SearchOptions::REGEX),
2631            replacement_text: r"$1\n",
2632            replace_all: true,
2633            expected_text: r#"
2634            A regular \n (shortened as regex \
2635            \\ regexp;[1] also referred to as
2636            rational \n[2][3]) is a sequence of characters that
2637            specifies a search
2638            pattern in text. Usually such patterns are used
2639            by string-searching algorithms
2640            for "find" or "find and replace" operations on strings, or for input validation.
2641            "#
2642            .unindent(),
2643        })
2644        .await;
2645    }
2646
2647    #[perf]
2648    #[gpui::test]
2649    async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
2650        cx: &mut TestAppContext,
2651    ) {
2652        init_globals(cx);
2653        let buffer = cx.new(|cx| {
2654            Buffer::local(
2655                r#"
2656                aaa bbb aaa ccc
2657                aaa bbb aaa ccc
2658                aaa bbb aaa ccc
2659                aaa bbb aaa ccc
2660                aaa bbb aaa ccc
2661                aaa bbb aaa ccc
2662                "#
2663                .unindent(),
2664                cx,
2665            )
2666        });
2667        let cx = cx.add_empty_window();
2668        let editor =
2669            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
2670
2671        let search_bar = cx.new_window_entity(|window, cx| {
2672            let mut search_bar = BufferSearchBar::new(None, window, cx);
2673            search_bar.set_active_pane_item(Some(&editor), window, cx);
2674            search_bar.show(window, cx);
2675            search_bar
2676        });
2677
2678        editor.update_in(cx, |editor, window, cx| {
2679            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2680                s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
2681            })
2682        });
2683
2684        search_bar.update_in(cx, |search_bar, window, cx| {
2685            let deploy = Deploy {
2686                focus: true,
2687                replace_enabled: false,
2688                selection_search_enabled: true,
2689            };
2690            search_bar.deploy(&deploy, window, cx);
2691        });
2692
2693        cx.run_until_parked();
2694
2695        search_bar
2696            .update_in(cx, |search_bar, window, cx| {
2697                search_bar.search("aaa", None, true, window, cx)
2698            })
2699            .await
2700            .unwrap();
2701
2702        editor.update(cx, |editor, cx| {
2703            assert_eq!(
2704                editor.search_background_highlights(cx),
2705                &[
2706                    Point::new(1, 0)..Point::new(1, 3),
2707                    Point::new(1, 8)..Point::new(1, 11),
2708                    Point::new(2, 0)..Point::new(2, 3),
2709                ]
2710            );
2711        });
2712    }
2713
2714    #[perf]
2715    #[gpui::test]
2716    async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
2717        cx: &mut TestAppContext,
2718    ) {
2719        init_globals(cx);
2720        let text = r#"
2721            aaa bbb aaa ccc
2722            aaa bbb aaa ccc
2723            aaa bbb aaa ccc
2724            aaa bbb aaa ccc
2725            aaa bbb aaa ccc
2726            aaa bbb aaa ccc
2727
2728            aaa bbb aaa ccc
2729            aaa bbb aaa ccc
2730            aaa bbb aaa ccc
2731            aaa bbb aaa ccc
2732            aaa bbb aaa ccc
2733            aaa bbb aaa ccc
2734            "#
2735        .unindent();
2736
2737        let cx = cx.add_empty_window();
2738        let editor = cx.new_window_entity(|window, cx| {
2739            let multibuffer = MultiBuffer::build_multi(
2740                [
2741                    (
2742                        &text,
2743                        vec![
2744                            Point::new(0, 0)..Point::new(2, 0),
2745                            Point::new(4, 0)..Point::new(5, 0),
2746                        ],
2747                    ),
2748                    (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
2749                ],
2750                cx,
2751            );
2752            Editor::for_multibuffer(multibuffer, None, window, cx)
2753        });
2754
2755        let search_bar = cx.new_window_entity(|window, cx| {
2756            let mut search_bar = BufferSearchBar::new(None, window, cx);
2757            search_bar.set_active_pane_item(Some(&editor), window, cx);
2758            search_bar.show(window, cx);
2759            search_bar
2760        });
2761
2762        editor.update_in(cx, |editor, window, cx| {
2763            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2764                s.select_ranges(vec![
2765                    Point::new(1, 0)..Point::new(1, 4),
2766                    Point::new(5, 3)..Point::new(6, 4),
2767                ])
2768            })
2769        });
2770
2771        search_bar.update_in(cx, |search_bar, window, cx| {
2772            let deploy = Deploy {
2773                focus: true,
2774                replace_enabled: false,
2775                selection_search_enabled: true,
2776            };
2777            search_bar.deploy(&deploy, window, cx);
2778        });
2779
2780        cx.run_until_parked();
2781
2782        search_bar
2783            .update_in(cx, |search_bar, window, cx| {
2784                search_bar.search("aaa", None, true, window, cx)
2785            })
2786            .await
2787            .unwrap();
2788
2789        editor.update(cx, |editor, cx| {
2790            assert_eq!(
2791                editor.search_background_highlights(cx),
2792                &[
2793                    Point::new(1, 0)..Point::new(1, 3),
2794                    Point::new(5, 8)..Point::new(5, 11),
2795                    Point::new(6, 0)..Point::new(6, 3),
2796                ]
2797            );
2798        });
2799    }
2800
2801    #[perf]
2802    #[gpui::test]
2803    async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
2804        let (editor, search_bar, cx) = init_test(cx);
2805        // Search using valid regexp
2806        search_bar
2807            .update_in(cx, |search_bar, window, cx| {
2808                search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
2809                search_bar.search("expression", None, true, window, cx)
2810            })
2811            .await
2812            .unwrap();
2813        editor.update_in(cx, |editor, window, cx| {
2814            assert_eq!(
2815                display_points_of(editor.all_text_background_highlights(window, cx)),
2816                &[
2817                    DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
2818                    DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
2819                ],
2820            );
2821        });
2822
2823        // Now, the expression is invalid
2824        search_bar
2825            .update_in(cx, |search_bar, window, cx| {
2826                search_bar.search("expression (", None, true, window, cx)
2827            })
2828            .await
2829            .unwrap_err();
2830        editor.update_in(cx, |editor, window, cx| {
2831            assert!(
2832                display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
2833            );
2834        });
2835    }
2836
2837    #[perf]
2838    #[gpui::test]
2839    async fn test_search_options_changes(cx: &mut TestAppContext) {
2840        let (_editor, search_bar, cx) = init_test(cx);
2841        update_search_settings(
2842            SearchSettings {
2843                button: true,
2844                whole_word: false,
2845                case_sensitive: false,
2846                include_ignored: false,
2847                regex: false,
2848                center_on_match: false,
2849            },
2850            cx,
2851        );
2852
2853        let deploy = Deploy {
2854            focus: true,
2855            replace_enabled: false,
2856            selection_search_enabled: true,
2857        };
2858
2859        search_bar.update_in(cx, |search_bar, window, cx| {
2860            assert_eq!(
2861                search_bar.search_options,
2862                SearchOptions::NONE,
2863                "Should have no search options enabled by default"
2864            );
2865            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2866            assert_eq!(
2867                search_bar.search_options,
2868                SearchOptions::WHOLE_WORD,
2869                "Should enable the option toggled"
2870            );
2871            assert!(
2872                !search_bar.dismissed,
2873                "Search bar should be present and visible"
2874            );
2875            search_bar.deploy(&deploy, window, cx);
2876            assert_eq!(
2877                search_bar.search_options,
2878                SearchOptions::WHOLE_WORD,
2879                "After (re)deploying, the option should still be enabled"
2880            );
2881
2882            search_bar.dismiss(&Dismiss, window, cx);
2883            search_bar.deploy(&deploy, window, cx);
2884            assert_eq!(
2885                search_bar.search_options,
2886                SearchOptions::WHOLE_WORD,
2887                "After hiding and showing the search bar, search options should be preserved"
2888            );
2889
2890            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
2891            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2892            assert_eq!(
2893                search_bar.search_options,
2894                SearchOptions::REGEX,
2895                "Should enable the options toggled"
2896            );
2897            assert!(
2898                !search_bar.dismissed,
2899                "Search bar should be present and visible"
2900            );
2901            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2902        });
2903
2904        update_search_settings(
2905            SearchSettings {
2906                button: true,
2907                whole_word: false,
2908                case_sensitive: true,
2909                include_ignored: false,
2910                regex: false,
2911                center_on_match: false,
2912            },
2913            cx,
2914        );
2915        search_bar.update_in(cx, |search_bar, window, cx| {
2916            assert_eq!(
2917                search_bar.search_options,
2918                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2919                "Should have no search options enabled by default"
2920            );
2921
2922            search_bar.deploy(&deploy, window, cx);
2923            assert_eq!(
2924                search_bar.search_options,
2925                SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
2926                "Toggling a non-dismissed search bar with custom options should not change the default options"
2927            );
2928            search_bar.dismiss(&Dismiss, window, cx);
2929            search_bar.deploy(&deploy, window, cx);
2930            assert_eq!(
2931                search_bar.configured_options,
2932                SearchOptions::CASE_SENSITIVE,
2933                "After a settings update and toggling the search bar, configured options should be updated"
2934            );
2935            assert_eq!(
2936                search_bar.search_options,
2937                SearchOptions::CASE_SENSITIVE,
2938                "After a settings update and toggling the search bar, configured options should be used"
2939            );
2940        });
2941
2942        update_search_settings(
2943            SearchSettings {
2944                button: true,
2945                whole_word: true,
2946                case_sensitive: true,
2947                include_ignored: false,
2948                regex: false,
2949                center_on_match: false,
2950            },
2951            cx,
2952        );
2953
2954        search_bar.update_in(cx, |search_bar, window, cx| {
2955            search_bar.deploy(&deploy, window, cx);
2956            search_bar.dismiss(&Dismiss, window, cx);
2957            search_bar.show(window, cx);
2958            assert_eq!(
2959                search_bar.search_options,
2960                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
2961                "Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
2962            );
2963        });
2964    }
2965
2966    fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
2967        cx.update(|cx| {
2968            SettingsStore::update_global(cx, |store, cx| {
2969                store.update_user_settings(cx, |settings| {
2970                    settings.editor.search = Some(SearchSettingsContent {
2971                        button: Some(search_settings.button),
2972                        whole_word: Some(search_settings.whole_word),
2973                        case_sensitive: Some(search_settings.case_sensitive),
2974                        include_ignored: Some(search_settings.include_ignored),
2975                        regex: Some(search_settings.regex),
2976                        center_on_match: Some(search_settings.center_on_match),
2977                    });
2978                });
2979            });
2980        });
2981    }
2982}