buffer_search.rs

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