buffer_search.rs

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