buffer_search.rs

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