buffer_search.rs

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