buffer_search.rs

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