buffer_search.rs

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