buffer_search.rs

   1use crate::{
   2    history::SearchHistory,
   3    mode::{next_mode, SearchMode},
   4    search_bar::render_nav_button,
   5    ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery,
   6    ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
   7    ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
   8};
   9use collections::HashMap;
  10use editor::{Editor, EditorElement, EditorStyle, Tab};
  11use futures::channel::oneshot;
  12use gpui::{
  13    actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView,
  14    FontStyle, FontWeight, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
  15    Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _,
  16    WhiteSpace, WindowContext,
  17};
  18use project::search::SearchQuery;
  19use serde::Deserialize;
  20use settings::Settings;
  21use std::{any::Any, sync::Arc};
  22use theme::ThemeSettings;
  23
  24use ui::{h_flex, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip};
  25use util::ResultExt;
  26use workspace::{
  27    item::ItemHandle,
  28    searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
  29    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
  30};
  31
  32#[derive(PartialEq, Clone, Deserialize)]
  33pub struct Deploy {
  34    pub focus: bool,
  35}
  36
  37impl_actions!(buffer_search, [Deploy]);
  38
  39actions!(buffer_search, [Dismiss, FocusEditor]);
  40
  41pub enum Event {
  42    UpdateLocation,
  43}
  44
  45pub fn init(cx: &mut AppContext) {
  46    cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
  47        .detach();
  48}
  49
  50pub struct BufferSearchBar {
  51    query_editor: View<Editor>,
  52    replacement_editor: View<Editor>,
  53    active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
  54    active_match_index: Option<usize>,
  55    active_searchable_item_subscription: Option<Subscription>,
  56    active_search: Option<Arc<SearchQuery>>,
  57    searchable_items_with_matches:
  58        HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
  59    pending_search: Option<Task<()>>,
  60    search_options: SearchOptions,
  61    default_options: SearchOptions,
  62    query_contains_error: bool,
  63    dismissed: bool,
  64    search_history: SearchHistory,
  65    current_mode: SearchMode,
  66    replace_enabled: bool,
  67}
  68
  69impl BufferSearchBar {
  70    fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
  71        let settings = ThemeSettings::get_global(cx);
  72        let text_style = TextStyle {
  73            color: if editor.read(cx).read_only(cx) {
  74                cx.theme().colors().text_disabled
  75            } else {
  76                cx.theme().colors().text
  77            },
  78            font_family: settings.ui_font.family.clone(),
  79            font_features: settings.ui_font.features,
  80            font_size: rems(0.875).into(),
  81            font_weight: FontWeight::NORMAL,
  82            font_style: FontStyle::Normal,
  83            line_height: relative(1.3).into(),
  84            background_color: None,
  85            underline: None,
  86            white_space: WhiteSpace::Normal,
  87        };
  88
  89        EditorElement::new(
  90            &editor,
  91            EditorStyle {
  92                background: cx.theme().colors().editor_background,
  93                local_player: cx.theme().players().local(),
  94                text: text_style,
  95                ..Default::default()
  96            },
  97        )
  98    }
  99}
 100
 101impl EventEmitter<Event> for BufferSearchBar {}
 102impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 103impl Render for BufferSearchBar {
 104    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 105        if self.dismissed {
 106            return div();
 107        }
 108
 109        let supported_options = self.supported_options();
 110
 111        if self.query_editor.read(cx).placeholder_text().is_none() {
 112            let query_focus_handle = self.query_editor.focus_handle(cx);
 113            let up_keystrokes = cx
 114                .bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle)
 115                .into_iter()
 116                .next()
 117                .map(|binding| {
 118                    binding
 119                        .keystrokes()
 120                        .iter()
 121                        .map(|k| k.to_string())
 122                        .collect::<Vec<_>>()
 123                });
 124            let down_keystrokes = cx
 125                .bindings_for_action_in(&NextHistoryQuery {}, &query_focus_handle)
 126                .into_iter()
 127                .next()
 128                .map(|binding| {
 129                    binding
 130                        .keystrokes()
 131                        .iter()
 132                        .map(|k| k.to_string())
 133                        .collect::<Vec<_>>()
 134                });
 135
 136            let placeholder_text =
 137                up_keystrokes
 138                    .zip(down_keystrokes)
 139                    .map(|(up_keystrokes, down_keystrokes)| {
 140                        Arc::from(format!(
 141                            "Search ({}/{} for previous/next query)",
 142                            up_keystrokes.join(" "),
 143                            down_keystrokes.join(" ")
 144                        ))
 145                    });
 146
 147            if let Some(placeholder_text) = placeholder_text {
 148                self.query_editor.update(cx, |editor, cx| {
 149                    editor.set_placeholder_text(placeholder_text, cx);
 150                });
 151            }
 152        }
 153
 154        self.replacement_editor.update(cx, |editor, cx| {
 155            editor.set_placeholder_text("Replace with...", cx);
 156        });
 157
 158        let match_count = self
 159            .active_searchable_item
 160            .as_ref()
 161            .and_then(|searchable_item| {
 162                if self.query(cx).is_empty() {
 163                    return None;
 164                }
 165                let matches = self
 166                    .searchable_items_with_matches
 167                    .get(&searchable_item.downgrade())?;
 168                let message = if let Some(match_ix) = self.active_match_index {
 169                    format!("{}/{}", match_ix + 1, matches.len())
 170                } else {
 171                    "No matches".to_string()
 172                };
 173
 174                Some(ui::Label::new(message))
 175            });
 176        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 177        let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
 178
 179        let mut key_context = KeyContext::default();
 180        key_context.add("BufferSearchBar");
 181        if in_replace {
 182            key_context.add("in_replace");
 183        }
 184        let editor_border = if self.query_contains_error {
 185            Color::Error.color(cx)
 186        } else {
 187            cx.theme().colors().border
 188        };
 189        h_flex()
 190            .w_full()
 191            .gap_2()
 192            .key_context(key_context)
 193            .capture_action(cx.listener(Self::tab))
 194            .on_action(cx.listener(Self::previous_history_query))
 195            .on_action(cx.listener(Self::next_history_query))
 196            .on_action(cx.listener(Self::dismiss))
 197            .on_action(cx.listener(Self::select_next_match))
 198            .on_action(cx.listener(Self::select_prev_match))
 199            .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
 200                this.activate_search_mode(SearchMode::Regex, cx);
 201            }))
 202            .on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
 203                this.activate_search_mode(SearchMode::Text, cx);
 204            }))
 205            .when(self.supported_options().replacement, |this| {
 206                this.on_action(cx.listener(Self::toggle_replace))
 207                    .when(in_replace, |this| {
 208                        this.on_action(cx.listener(Self::replace_next))
 209                            .on_action(cx.listener(Self::replace_all))
 210                    })
 211            })
 212            .when(self.supported_options().case, |this| {
 213                this.on_action(cx.listener(Self::toggle_case_sensitive))
 214            })
 215            .when(self.supported_options().word, |this| {
 216                this.on_action(cx.listener(Self::toggle_whole_word))
 217            })
 218            .child(
 219                h_flex()
 220                    .flex_1()
 221                    .px_2()
 222                    .py_1()
 223                    .gap_2()
 224                    .border_1()
 225                    .border_color(editor_border)
 226                    .min_w(rems(384. / 16.))
 227                    .rounded_lg()
 228                    .child(Icon::new(IconName::MagnifyingGlass))
 229                    .child(self.render_text_input(&self.query_editor, cx))
 230                    .children(supported_options.case.then(|| {
 231                        self.render_search_option_button(
 232                            SearchOptions::CASE_SENSITIVE,
 233                            cx.listener(|this, _, cx| {
 234                                this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
 235                            }),
 236                        )
 237                    }))
 238                    .children(supported_options.word.then(|| {
 239                        self.render_search_option_button(
 240                            SearchOptions::WHOLE_WORD,
 241                            cx.listener(|this, _, cx| this.toggle_whole_word(&ToggleWholeWord, cx)),
 242                        )
 243                    })),
 244            )
 245            .child(
 246                h_flex()
 247                    .gap_2()
 248                    .flex_none()
 249                    .child(
 250                        h_flex()
 251                            .child(
 252                                ToggleButton::new("search-mode-text", SearchMode::Text.label())
 253                                    .style(ButtonStyle::Filled)
 254                                    .size(ButtonSize::Large)
 255                                    .selected(self.current_mode == SearchMode::Text)
 256                                    .on_click(cx.listener(move |_, _event, cx| {
 257                                        cx.dispatch_action(SearchMode::Text.action())
 258                                    }))
 259                                    .tooltip(|cx| {
 260                                        Tooltip::for_action(
 261                                            SearchMode::Text.tooltip(),
 262                                            &*SearchMode::Text.action(),
 263                                            cx,
 264                                        )
 265                                    })
 266                                    .first(),
 267                            )
 268                            .child(
 269                                ToggleButton::new("search-mode-regex", SearchMode::Regex.label())
 270                                    .style(ButtonStyle::Filled)
 271                                    .size(ButtonSize::Large)
 272                                    .selected(self.current_mode == SearchMode::Regex)
 273                                    .on_click(cx.listener(move |_, _event, cx| {
 274                                        cx.dispatch_action(SearchMode::Regex.action())
 275                                    }))
 276                                    .tooltip(|cx| {
 277                                        Tooltip::for_action(
 278                                            SearchMode::Regex.tooltip(),
 279                                            &*SearchMode::Regex.action(),
 280                                            cx,
 281                                        )
 282                                    })
 283                                    .last(),
 284                            ),
 285                    )
 286                    .when(supported_options.replacement, |this| {
 287                        this.child(
 288                            IconButton::new(
 289                                "buffer-search-bar-toggle-replace-button",
 290                                IconName::Replace,
 291                            )
 292                            .style(ButtonStyle::Subtle)
 293                            .when(self.replace_enabled, |button| {
 294                                button.style(ButtonStyle::Filled)
 295                            })
 296                            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 297                                this.toggle_replace(&ToggleReplace, cx);
 298                            }))
 299                            .tooltip(|cx| {
 300                                Tooltip::for_action("Toggle replace", &ToggleReplace, cx)
 301                            }),
 302                        )
 303                    }),
 304            )
 305            .child(
 306                h_flex()
 307                    .gap_0p5()
 308                    .flex_1()
 309                    .when(self.replace_enabled, |this| {
 310                        this.child(
 311                            h_flex()
 312                                .flex_1()
 313                                // We're giving this a fixed height to match the height of the search input,
 314                                // which has an icon inside that is increasing its height.
 315                                .h_8()
 316                                .px_2()
 317                                .py_1()
 318                                .gap_2()
 319                                .border_1()
 320                                .border_color(cx.theme().colors().border)
 321                                .rounded_lg()
 322                                .child(self.render_text_input(&self.replacement_editor, cx)),
 323                        )
 324                        .when(should_show_replace_input, |this| {
 325                            this.child(
 326                                IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
 327                                    .tooltip(move |cx| {
 328                                        Tooltip::for_action("Replace next", &ReplaceNext, cx)
 329                                    })
 330                                    .on_click(cx.listener(|this, _, cx| {
 331                                        this.replace_next(&ReplaceNext, cx)
 332                                    })),
 333                            )
 334                            .child(
 335                                IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
 336                                    .tooltip(move |cx| {
 337                                        Tooltip::for_action("Replace all", &ReplaceAll, cx)
 338                                    })
 339                                    .on_click(
 340                                        cx.listener(|this, _, cx| {
 341                                            this.replace_all(&ReplaceAll, cx)
 342                                        }),
 343                                    ),
 344                            )
 345                        })
 346                    }),
 347            )
 348            .child(
 349                h_flex()
 350                    .gap_0p5()
 351                    .flex_none()
 352                    .child(
 353                        IconButton::new("select-all", ui::IconName::SelectAll)
 354                            .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
 355                            .tooltip(|cx| {
 356                                Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
 357                            }),
 358                    )
 359                    .children(match_count)
 360                    .child(render_nav_button(
 361                        ui::IconName::ChevronLeft,
 362                        self.active_match_index.is_some(),
 363                        "Select previous match",
 364                        &SelectPrevMatch,
 365                    ))
 366                    .child(render_nav_button(
 367                        ui::IconName::ChevronRight,
 368                        self.active_match_index.is_some(),
 369                        "Select next match",
 370                        &SelectNextMatch,
 371                    )),
 372            )
 373    }
 374}
 375
 376impl FocusableView for BufferSearchBar {
 377    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 378        self.query_editor.focus_handle(cx)
 379    }
 380}
 381
 382impl ToolbarItemView for BufferSearchBar {
 383    fn set_active_pane_item(
 384        &mut self,
 385        item: Option<&dyn ItemHandle>,
 386        cx: &mut ViewContext<Self>,
 387    ) -> ToolbarItemLocation {
 388        cx.notify();
 389        self.active_searchable_item_subscription.take();
 390        self.active_searchable_item.take();
 391
 392        self.pending_search.take();
 393
 394        if let Some(searchable_item_handle) =
 395            item.and_then(|item| item.to_searchable_item_handle(cx))
 396        {
 397            let this = cx.view().downgrade();
 398
 399            searchable_item_handle
 400                .subscribe_to_search_events(
 401                    cx,
 402                    Box::new(move |search_event, cx| {
 403                        if let Some(this) = this.upgrade() {
 404                            this.update(cx, |this, cx| {
 405                                this.on_active_searchable_item_event(search_event, cx)
 406                            });
 407                        }
 408                    }),
 409                )
 410                .detach();
 411
 412            self.active_searchable_item = Some(searchable_item_handle);
 413            let _ = self.update_matches(cx);
 414            if !self.dismissed {
 415                return ToolbarItemLocation::Secondary;
 416            }
 417        }
 418        ToolbarItemLocation::Hidden
 419    }
 420
 421    fn row_count(&self, _: &WindowContext<'_>) -> usize {
 422        1
 423    }
 424}
 425
 426/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
 427pub trait SearchActionsRegistrar {
 428    fn register_handler<A: Action>(
 429        &mut self,
 430        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 431    );
 432
 433    fn register_handler_for_dismissed_bar<A: Action>(
 434        &mut self,
 435        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 436    );
 437}
 438
 439type GetSearchBar<T> =
 440    for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
 441
 442/// Registers search actions on a div that can be taken out.
 443pub struct DivRegistrar<'a, 'b, T: 'static> {
 444    div: Option<Div>,
 445    cx: &'a mut ViewContext<'b, T>,
 446    search_getter: GetSearchBar<T>,
 447}
 448
 449impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
 450    pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
 451        Self {
 452            div: Some(div()),
 453            cx,
 454            search_getter,
 455        }
 456    }
 457    pub fn into_div(self) -> Div {
 458        // This option is always Some; it's an option in the first place because we want to call methods
 459        // on div that require ownership.
 460        self.div.unwrap()
 461    }
 462}
 463
 464impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
 465    fn register_handler<A: Action>(
 466        &mut self,
 467        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 468    ) {
 469        let getter = self.search_getter;
 470        self.div = self.div.take().map(|div| {
 471            div.on_action(self.cx.listener(move |this, action, cx| {
 472                let should_notify = (getter)(this, cx)
 473                    .clone()
 474                    .map(|search_bar| {
 475                        search_bar.update(cx, |search_bar, cx| {
 476                            if search_bar.is_dismissed() {
 477                                false
 478                            } else {
 479                                callback(search_bar, action, cx);
 480                                true
 481                            }
 482                        })
 483                    })
 484                    .unwrap_or(false);
 485                if should_notify {
 486                    cx.notify();
 487                } else {
 488                    cx.propagate();
 489                }
 490            }))
 491        });
 492    }
 493
 494    fn register_handler_for_dismissed_bar<A: Action>(
 495        &mut self,
 496        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 497    ) {
 498        let getter = self.search_getter;
 499        self.div = self.div.take().map(|div| {
 500            div.on_action(self.cx.listener(move |this, action, cx| {
 501                let should_notify = (getter)(this, cx)
 502                    .clone()
 503                    .map(|search_bar| {
 504                        search_bar.update(cx, |search_bar, cx| {
 505                            if search_bar.is_dismissed() {
 506                                callback(search_bar, action, cx);
 507                                true
 508                            } else {
 509                                false
 510                            }
 511                        })
 512                    })
 513                    .unwrap_or(false);
 514                if should_notify {
 515                    cx.notify();
 516                } else {
 517                    cx.propagate();
 518                }
 519            }))
 520        });
 521    }
 522}
 523
 524/// Register actions for an active pane.
 525impl SearchActionsRegistrar for Workspace {
 526    fn register_handler<A: Action>(
 527        &mut self,
 528        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 529    ) {
 530        self.register_action(move |workspace, action: &A, cx| {
 531            if workspace.has_active_modal(cx) {
 532                cx.propagate();
 533                return;
 534            }
 535
 536            let pane = workspace.active_pane();
 537            pane.update(cx, move |this, cx| {
 538                this.toolbar().update(cx, move |this, cx| {
 539                    if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
 540                        let should_notify = search_bar.update(cx, move |search_bar, cx| {
 541                            if search_bar.is_dismissed() {
 542                                false
 543                            } else {
 544                                callback(search_bar, action, cx);
 545                                true
 546                            }
 547                        });
 548                        if should_notify {
 549                            cx.notify();
 550                        } else {
 551                            cx.propagate();
 552                        }
 553                    }
 554                })
 555            });
 556        });
 557    }
 558
 559    fn register_handler_for_dismissed_bar<A: Action>(
 560        &mut self,
 561        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 562    ) {
 563        self.register_action(move |workspace, action: &A, cx| {
 564            if workspace.has_active_modal(cx) {
 565                cx.propagate();
 566                return;
 567            }
 568
 569            let pane = workspace.active_pane();
 570            pane.update(cx, move |this, cx| {
 571                this.toolbar().update(cx, move |this, cx| {
 572                    if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
 573                        let should_notify = search_bar.update(cx, move |search_bar, cx| {
 574                            if search_bar.is_dismissed() {
 575                                callback(search_bar, action, cx);
 576                                true
 577                            } else {
 578                                false
 579                            }
 580                        });
 581                        if should_notify {
 582                            cx.notify();
 583                        } else {
 584                            cx.propagate();
 585                        }
 586                    }
 587                })
 588            });
 589        });
 590    }
 591}
 592
 593impl BufferSearchBar {
 594    pub fn register(registrar: &mut impl SearchActionsRegistrar) {
 595        registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
 596            if this.supported_options().case {
 597                this.toggle_case_sensitive(action, cx);
 598            }
 599        });
 600        registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
 601            if this.supported_options().word {
 602                this.toggle_whole_word(action, cx);
 603            }
 604        });
 605        registrar.register_handler(|this, action: &ToggleReplace, cx| {
 606            if this.supported_options().replacement {
 607                this.toggle_replace(action, cx);
 608            }
 609        });
 610        registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
 611            if this.supported_options().regex {
 612                this.activate_search_mode(SearchMode::Regex, cx);
 613            }
 614        });
 615        registrar.register_handler(|this, _: &ActivateTextMode, cx| {
 616            this.activate_search_mode(SearchMode::Text, cx);
 617        });
 618        registrar.register_handler(|this, action: &CycleMode, cx| {
 619            if this.supported_options().regex {
 620                // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
 621                // cycling.
 622                this.cycle_mode(action, cx)
 623            }
 624        });
 625        registrar.register_handler(|this, action: &SelectNextMatch, cx| {
 626            this.select_next_match(action, cx);
 627        });
 628        registrar.register_handler(|this, action: &SelectPrevMatch, cx| {
 629            this.select_prev_match(action, cx);
 630        });
 631        registrar.register_handler(|this, action: &SelectAllMatches, cx| {
 632            this.select_all_matches(action, cx);
 633        });
 634        registrar.register_handler(|this, _: &editor::Cancel, cx| {
 635            this.dismiss(&Dismiss, cx);
 636        });
 637        registrar.register_handler_for_dismissed_bar(|this, deploy, cx| {
 638            this.deploy(deploy, cx);
 639        })
 640    }
 641
 642    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 643        let query_editor = cx.new_view(|cx| Editor::single_line(cx));
 644        cx.subscribe(&query_editor, Self::on_query_editor_event)
 645            .detach();
 646        let replacement_editor = cx.new_view(|cx| Editor::single_line(cx));
 647        cx.subscribe(&replacement_editor, Self::on_query_editor_event)
 648            .detach();
 649        Self {
 650            query_editor,
 651            replacement_editor,
 652            active_searchable_item: None,
 653            active_searchable_item_subscription: None,
 654            active_match_index: None,
 655            searchable_items_with_matches: Default::default(),
 656            default_options: SearchOptions::NONE,
 657            search_options: SearchOptions::NONE,
 658            pending_search: None,
 659            query_contains_error: false,
 660            dismissed: true,
 661            search_history: SearchHistory::default(),
 662            current_mode: SearchMode::default(),
 663            active_search: None,
 664            replace_enabled: false,
 665        }
 666    }
 667
 668    pub fn is_dismissed(&self) -> bool {
 669        self.dismissed
 670    }
 671
 672    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 673        self.dismissed = true;
 674        for searchable_item in self.searchable_items_with_matches.keys() {
 675            if let Some(searchable_item) =
 676                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 677            {
 678                searchable_item.clear_matches(cx);
 679            }
 680        }
 681        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 682            let handle = active_editor.focus_handle(cx);
 683            cx.focus(&handle);
 684        }
 685        cx.emit(Event::UpdateLocation);
 686        cx.emit(ToolbarItemEvent::ChangeLocation(
 687            ToolbarItemLocation::Hidden,
 688        ));
 689        cx.notify();
 690    }
 691
 692    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 693        if self.show(cx) {
 694            self.search_suggested(cx);
 695            if deploy.focus {
 696                self.select_query(cx);
 697                let handle = self.query_editor.focus_handle(cx);
 698                cx.focus(&handle);
 699            }
 700            return true;
 701        }
 702
 703        false
 704    }
 705
 706    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
 707        if self.is_dismissed() {
 708            self.deploy(action, cx);
 709        } else {
 710            self.dismiss(&Dismiss, cx);
 711        }
 712    }
 713
 714    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 715        if self.active_searchable_item.is_none() {
 716            return false;
 717        }
 718        self.dismissed = false;
 719        cx.notify();
 720        cx.emit(Event::UpdateLocation);
 721        cx.emit(ToolbarItemEvent::ChangeLocation(
 722            ToolbarItemLocation::Secondary,
 723        ));
 724        true
 725    }
 726
 727    fn supported_options(&self) -> workspace::searchable::SearchOptions {
 728        self.active_searchable_item
 729            .as_deref()
 730            .map(SearchableItemHandle::supported_options)
 731            .unwrap_or_default()
 732    }
 733    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 734        let search = self
 735            .query_suggestion(cx)
 736            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 737
 738        if let Some(search) = search {
 739            cx.spawn(|this, mut cx| async move {
 740                search.await?;
 741                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 742            })
 743            .detach_and_log_err(cx);
 744        }
 745    }
 746
 747    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 748        if let Some(match_ix) = self.active_match_index {
 749            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 750                if let Some(matches) = self
 751                    .searchable_items_with_matches
 752                    .get(&active_searchable_item.downgrade())
 753                {
 754                    active_searchable_item.activate_match(match_ix, matches, cx)
 755                }
 756            }
 757        }
 758    }
 759
 760    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 761        self.query_editor.update(cx, |query_editor, cx| {
 762            query_editor.select_all(&Default::default(), cx);
 763        });
 764    }
 765
 766    pub fn query(&self, cx: &WindowContext) -> String {
 767        self.query_editor.read(cx).text(cx)
 768    }
 769    pub fn replacement(&self, cx: &WindowContext) -> String {
 770        self.replacement_editor.read(cx).text(cx)
 771    }
 772    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 773        self.active_searchable_item
 774            .as_ref()
 775            .map(|searchable_item| searchable_item.query_suggestion(cx))
 776            .filter(|suggestion| !suggestion.is_empty())
 777    }
 778
 779    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 780        if replacement.is_none() {
 781            self.replace_enabled = false;
 782            return;
 783        }
 784        self.replace_enabled = true;
 785        self.replacement_editor
 786            .update(cx, |replacement_editor, cx| {
 787                replacement_editor
 788                    .buffer()
 789                    .update(cx, |replacement_buffer, cx| {
 790                        let len = replacement_buffer.len(cx);
 791                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 792                    });
 793            });
 794    }
 795
 796    pub fn search(
 797        &mut self,
 798        query: &str,
 799        options: Option<SearchOptions>,
 800        cx: &mut ViewContext<Self>,
 801    ) -> oneshot::Receiver<()> {
 802        let options = options.unwrap_or(self.default_options);
 803        if query != self.query(cx) || self.search_options != options {
 804            self.query_editor.update(cx, |query_editor, cx| {
 805                query_editor.buffer().update(cx, |query_buffer, cx| {
 806                    let len = query_buffer.len(cx);
 807                    query_buffer.edit([(0..len, query)], None, cx);
 808                });
 809            });
 810            self.search_options = options;
 811            self.query_contains_error = false;
 812            self.clear_matches(cx);
 813            cx.notify();
 814        }
 815        self.update_matches(cx)
 816    }
 817
 818    fn render_search_option_button(
 819        &self,
 820        option: SearchOptions,
 821        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 822    ) -> impl IntoElement {
 823        let is_active = self.search_options.contains(option);
 824        option.as_button(is_active, action)
 825    }
 826    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 827        assert_ne!(
 828            mode,
 829            SearchMode::Semantic,
 830            "Semantic search is not supported in buffer search"
 831        );
 832        if mode == self.current_mode {
 833            return;
 834        }
 835        self.current_mode = mode;
 836        let _ = self.update_matches(cx);
 837        cx.notify();
 838    }
 839
 840    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 841        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 842            let handle = active_editor.focus_handle(cx);
 843            cx.focus(&handle);
 844        }
 845    }
 846
 847    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 848        self.search_options.toggle(search_option);
 849        self.default_options = self.search_options;
 850        let _ = self.update_matches(cx);
 851        cx.notify();
 852    }
 853
 854    pub fn set_search_options(
 855        &mut self,
 856        search_options: SearchOptions,
 857        cx: &mut ViewContext<Self>,
 858    ) {
 859        self.search_options = search_options;
 860        cx.notify();
 861    }
 862
 863    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 864        self.select_match(Direction::Next, 1, cx);
 865    }
 866
 867    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 868        self.select_match(Direction::Prev, 1, cx);
 869    }
 870
 871    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 872        if !self.dismissed && self.active_match_index.is_some() {
 873            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 874                if let Some(matches) = self
 875                    .searchable_items_with_matches
 876                    .get(&searchable_item.downgrade())
 877                {
 878                    searchable_item.select_matches(matches, cx);
 879                    self.focus_editor(&FocusEditor, cx);
 880                }
 881            }
 882        }
 883    }
 884
 885    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 886        if let Some(index) = self.active_match_index {
 887            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 888                if let Some(matches) = self
 889                    .searchable_items_with_matches
 890                    .get(&searchable_item.downgrade())
 891                {
 892                    let new_match_index = searchable_item
 893                        .match_index_for_direction(matches, index, direction, count, cx);
 894
 895                    searchable_item.update_matches(matches, cx);
 896                    searchable_item.activate_match(new_match_index, matches, cx);
 897                }
 898            }
 899        }
 900    }
 901
 902    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 903        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 904            if let Some(matches) = self
 905                .searchable_items_with_matches
 906                .get(&searchable_item.downgrade())
 907            {
 908                if matches.len() == 0 {
 909                    return;
 910                }
 911                let new_match_index = matches.len() - 1;
 912                searchable_item.update_matches(matches, cx);
 913                searchable_item.activate_match(new_match_index, matches, cx);
 914            }
 915        }
 916    }
 917
 918    fn on_query_editor_event(
 919        &mut self,
 920        _: View<Editor>,
 921        event: &editor::EditorEvent,
 922        cx: &mut ViewContext<Self>,
 923    ) {
 924        if let editor::EditorEvent::Edited { .. } = event {
 925            self.query_contains_error = false;
 926            self.clear_matches(cx);
 927            let search = self.update_matches(cx);
 928            cx.spawn(|this, mut cx| async move {
 929                search.await?;
 930                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 931            })
 932            .detach_and_log_err(cx);
 933        }
 934    }
 935
 936    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 937        match event {
 938            SearchEvent::MatchesInvalidated => {
 939                let _ = self.update_matches(cx);
 940            }
 941            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 942        }
 943    }
 944
 945    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 946        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 947    }
 948    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 949        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 950    }
 951    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 952        let mut active_item_matches = None;
 953        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 954            if let Some(searchable_item) =
 955                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 956            {
 957                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 958                    active_item_matches = Some((searchable_item.downgrade(), matches));
 959                } else {
 960                    searchable_item.clear_matches(cx);
 961                }
 962            }
 963        }
 964
 965        self.searchable_items_with_matches
 966            .extend(active_item_matches);
 967    }
 968
 969    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 970        let (done_tx, done_rx) = oneshot::channel();
 971        let query = self.query(cx);
 972        self.pending_search.take();
 973
 974        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 975            if query.is_empty() {
 976                self.active_match_index.take();
 977                active_searchable_item.clear_matches(cx);
 978                let _ = done_tx.send(());
 979                cx.notify();
 980            } else {
 981                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
 982                    match SearchQuery::regex(
 983                        query,
 984                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 985                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 986                        false,
 987                        Vec::new(),
 988                        Vec::new(),
 989                    ) {
 990                        Ok(query) => query.with_replacement(self.replacement(cx)),
 991                        Err(_) => {
 992                            self.query_contains_error = true;
 993                            self.active_match_index = None;
 994                            cx.notify();
 995                            return done_rx;
 996                        }
 997                    }
 998                } else {
 999                    match SearchQuery::text(
1000                        query,
1001                        self.search_options.contains(SearchOptions::WHOLE_WORD),
1002                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1003                        false,
1004                        Vec::new(),
1005                        Vec::new(),
1006                    ) {
1007                        Ok(query) => query.with_replacement(self.replacement(cx)),
1008                        Err(_) => {
1009                            self.query_contains_error = true;
1010                            self.active_match_index = None;
1011                            cx.notify();
1012                            return done_rx;
1013                        }
1014                    }
1015                }
1016                .into();
1017                self.active_search = Some(query.clone());
1018                let query_text = query.as_str().to_string();
1019
1020                let matches = active_searchable_item.find_matches(query, cx);
1021
1022                let active_searchable_item = active_searchable_item.downgrade();
1023                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
1024                    let matches = matches.await;
1025
1026                    this.update(&mut cx, |this, cx| {
1027                        if let Some(active_searchable_item) =
1028                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
1029                        {
1030                            this.searchable_items_with_matches
1031                                .insert(active_searchable_item.downgrade(), matches);
1032
1033                            this.update_match_index(cx);
1034                            this.search_history.add(query_text);
1035                            if !this.dismissed {
1036                                let matches = this
1037                                    .searchable_items_with_matches
1038                                    .get(&active_searchable_item.downgrade())
1039                                    .unwrap();
1040                                active_searchable_item.update_matches(matches, cx);
1041                                let _ = done_tx.send(());
1042                            }
1043                            cx.notify();
1044                        }
1045                    })
1046                    .log_err();
1047                }));
1048            }
1049        }
1050        done_rx
1051    }
1052
1053    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
1054        let new_index = self
1055            .active_searchable_item
1056            .as_ref()
1057            .and_then(|searchable_item| {
1058                let matches = self
1059                    .searchable_items_with_matches
1060                    .get(&searchable_item.downgrade())?;
1061                searchable_item.active_match_index(matches, cx)
1062            });
1063        if new_index != self.active_match_index {
1064            self.active_match_index = new_index;
1065            cx.notify();
1066        }
1067    }
1068
1069    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
1070        if let Some(item) = self.active_searchable_item.as_ref() {
1071            let focus_handle = item.focus_handle(cx);
1072            cx.focus(&focus_handle);
1073            cx.stop_propagation();
1074        }
1075    }
1076
1077    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1078        if let Some(new_query) = self.search_history.next().map(str::to_string) {
1079            let _ = self.search(&new_query, Some(self.search_options), cx);
1080        } else {
1081            self.search_history.reset_selection();
1082            let _ = self.search("", Some(self.search_options), cx);
1083        }
1084    }
1085
1086    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1087        if self.query(cx).is_empty() {
1088            if let Some(new_query) = self.search_history.current().map(str::to_string) {
1089                let _ = self.search(&new_query, Some(self.search_options), cx);
1090                return;
1091            }
1092        }
1093
1094        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
1095            let _ = self.search(&new_query, Some(self.search_options), cx);
1096        }
1097    }
1098    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
1099        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
1100    }
1101    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1102        if let Some(_) = &self.active_searchable_item {
1103            self.replace_enabled = !self.replace_enabled;
1104            if !self.replace_enabled {
1105                let handle = self.query_editor.focus_handle(cx);
1106                cx.focus(&handle);
1107            }
1108            cx.notify();
1109        }
1110    }
1111    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1112        let mut should_propagate = true;
1113        if !self.dismissed && self.active_search.is_some() {
1114            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1115                if let Some(query) = self.active_search.as_ref() {
1116                    if let Some(matches) = self
1117                        .searchable_items_with_matches
1118                        .get(&searchable_item.downgrade())
1119                    {
1120                        if let Some(active_index) = self.active_match_index {
1121                            let query = query
1122                                .as_ref()
1123                                .clone()
1124                                .with_replacement(self.replacement(cx));
1125                            searchable_item.replace(&matches[active_index], &query, cx);
1126                            self.select_next_match(&SelectNextMatch, cx);
1127                        }
1128                        should_propagate = false;
1129                        self.focus_editor(&FocusEditor, cx);
1130                    }
1131                }
1132            }
1133        }
1134        if !should_propagate {
1135            cx.stop_propagation();
1136        }
1137    }
1138    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1139        if !self.dismissed && self.active_search.is_some() {
1140            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1141                if let Some(query) = self.active_search.as_ref() {
1142                    if let Some(matches) = self
1143                        .searchable_items_with_matches
1144                        .get(&searchable_item.downgrade())
1145                    {
1146                        let query = query
1147                            .as_ref()
1148                            .clone()
1149                            .with_replacement(self.replacement(cx));
1150                        for m in matches {
1151                            searchable_item.replace(m, &query, cx);
1152                        }
1153                    }
1154                }
1155            }
1156        }
1157    }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use std::ops::Range;
1163
1164    use super::*;
1165    use editor::{DisplayPoint, Editor};
1166    use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
1167    use language::Buffer;
1168    use smol::stream::StreamExt as _;
1169    use unindent::Unindent as _;
1170
1171    fn init_globals(cx: &mut TestAppContext) {
1172        cx.update(|cx| {
1173            let store = settings::SettingsStore::test(cx);
1174            cx.set_global(store);
1175            editor::init(cx);
1176
1177            language::init(cx);
1178            theme::init(theme::LoadThemes::JustBase, cx);
1179        });
1180    }
1181
1182    fn init_test(
1183        cx: &mut TestAppContext,
1184    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1185        init_globals(cx);
1186        let buffer = cx.new_model(|cx| {
1187            Buffer::new(
1188                0,
1189                cx.entity_id().as_u64(),
1190                r#"
1191                A regular expression (shortened as regex or regexp;[1] also referred to as
1192                rational expression[2][3]) is a sequence of characters that specifies a search
1193                pattern in text. Usually such patterns are used by string-searching algorithms
1194                for "find" or "find and replace" operations on strings, or for input validation.
1195                "#
1196                .unindent(),
1197            )
1198        });
1199        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1200        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1201
1202        let search_bar = cx.new_view(|cx| {
1203            let mut search_bar = BufferSearchBar::new(cx);
1204            search_bar.set_active_pane_item(Some(&editor), cx);
1205            search_bar.show(cx);
1206            search_bar
1207        });
1208
1209        (editor, search_bar, cx)
1210    }
1211
1212    #[gpui::test]
1213    async fn test_search_simple(cx: &mut TestAppContext) {
1214        let (editor, search_bar, cx) = init_test(cx);
1215        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1216            background_highlights
1217                .into_iter()
1218                .map(|(range, _)| range)
1219                .collect::<Vec<_>>()
1220        };
1221        // Search for a string that appears with different casing.
1222        // By default, search is case-insensitive.
1223        search_bar
1224            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1225            .await
1226            .unwrap();
1227        editor.update(cx, |editor, cx| {
1228            assert_eq!(
1229                display_points_of(editor.all_text_background_highlights(cx)),
1230                &[
1231                    DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1232                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1233                ]
1234            );
1235        });
1236
1237        // Switch to a case sensitive search.
1238        search_bar.update(cx, |search_bar, cx| {
1239            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1240        });
1241        let mut editor_notifications = cx.notifications(&editor);
1242        editor_notifications.next().await;
1243        editor.update(cx, |editor, cx| {
1244            assert_eq!(
1245                display_points_of(editor.all_text_background_highlights(cx)),
1246                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1247            );
1248        });
1249
1250        // Search for a string that appears both as a whole word and
1251        // within other words. By default, all results are found.
1252        search_bar
1253            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1254            .await
1255            .unwrap();
1256        editor.update(cx, |editor, cx| {
1257            assert_eq!(
1258                display_points_of(editor.all_text_background_highlights(cx)),
1259                &[
1260                    DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1261                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1262                    DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1263                    DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1264                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1265                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1266                    DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1267                ]
1268            );
1269        });
1270
1271        // Switch to a whole word search.
1272        search_bar.update(cx, |search_bar, cx| {
1273            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1274        });
1275        let mut editor_notifications = cx.notifications(&editor);
1276        editor_notifications.next().await;
1277        editor.update(cx, |editor, cx| {
1278            assert_eq!(
1279                display_points_of(editor.all_text_background_highlights(cx)),
1280                &[
1281                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1282                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1283                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1284                ]
1285            );
1286        });
1287
1288        editor.update(cx, |editor, cx| {
1289            editor.change_selections(None, cx, |s| {
1290                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1291            });
1292        });
1293        search_bar.update(cx, |search_bar, cx| {
1294            assert_eq!(search_bar.active_match_index, Some(0));
1295            search_bar.select_next_match(&SelectNextMatch, cx);
1296            assert_eq!(
1297                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1298                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1299            );
1300        });
1301        search_bar.update(cx, |search_bar, _| {
1302            assert_eq!(search_bar.active_match_index, Some(0));
1303        });
1304
1305        search_bar.update(cx, |search_bar, cx| {
1306            search_bar.select_next_match(&SelectNextMatch, cx);
1307            assert_eq!(
1308                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1309                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1310            );
1311        });
1312        search_bar.update(cx, |search_bar, _| {
1313            assert_eq!(search_bar.active_match_index, Some(1));
1314        });
1315
1316        search_bar.update(cx, |search_bar, cx| {
1317            search_bar.select_next_match(&SelectNextMatch, cx);
1318            assert_eq!(
1319                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1320                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1321            );
1322        });
1323        search_bar.update(cx, |search_bar, _| {
1324            assert_eq!(search_bar.active_match_index, Some(2));
1325        });
1326
1327        search_bar.update(cx, |search_bar, cx| {
1328            search_bar.select_next_match(&SelectNextMatch, cx);
1329            assert_eq!(
1330                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1331                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1332            );
1333        });
1334        search_bar.update(cx, |search_bar, _| {
1335            assert_eq!(search_bar.active_match_index, Some(0));
1336        });
1337
1338        search_bar.update(cx, |search_bar, cx| {
1339            search_bar.select_prev_match(&SelectPrevMatch, cx);
1340            assert_eq!(
1341                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1342                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1343            );
1344        });
1345        search_bar.update(cx, |search_bar, _| {
1346            assert_eq!(search_bar.active_match_index, Some(2));
1347        });
1348
1349        search_bar.update(cx, |search_bar, cx| {
1350            search_bar.select_prev_match(&SelectPrevMatch, cx);
1351            assert_eq!(
1352                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1353                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1354            );
1355        });
1356        search_bar.update(cx, |search_bar, _| {
1357            assert_eq!(search_bar.active_match_index, Some(1));
1358        });
1359
1360        search_bar.update(cx, |search_bar, cx| {
1361            search_bar.select_prev_match(&SelectPrevMatch, cx);
1362            assert_eq!(
1363                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1364                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1365            );
1366        });
1367        search_bar.update(cx, |search_bar, _| {
1368            assert_eq!(search_bar.active_match_index, Some(0));
1369        });
1370
1371        // Park the cursor in between matches and ensure that going to the previous match selects
1372        // the closest match to the left.
1373        editor.update(cx, |editor, cx| {
1374            editor.change_selections(None, cx, |s| {
1375                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1376            });
1377        });
1378        search_bar.update(cx, |search_bar, cx| {
1379            assert_eq!(search_bar.active_match_index, Some(1));
1380            search_bar.select_prev_match(&SelectPrevMatch, cx);
1381            assert_eq!(
1382                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1383                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1384            );
1385        });
1386        search_bar.update(cx, |search_bar, _| {
1387            assert_eq!(search_bar.active_match_index, Some(0));
1388        });
1389
1390        // Park the cursor in between matches and ensure that going to the next match selects the
1391        // closest match to the right.
1392        editor.update(cx, |editor, cx| {
1393            editor.change_selections(None, cx, |s| {
1394                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1395            });
1396        });
1397        search_bar.update(cx, |search_bar, cx| {
1398            assert_eq!(search_bar.active_match_index, Some(1));
1399            search_bar.select_next_match(&SelectNextMatch, cx);
1400            assert_eq!(
1401                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1402                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1403            );
1404        });
1405        search_bar.update(cx, |search_bar, _| {
1406            assert_eq!(search_bar.active_match_index, Some(1));
1407        });
1408
1409        // Park the cursor after the last match and ensure that going to the previous match selects
1410        // the last match.
1411        editor.update(cx, |editor, cx| {
1412            editor.change_selections(None, cx, |s| {
1413                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1414            });
1415        });
1416        search_bar.update(cx, |search_bar, cx| {
1417            assert_eq!(search_bar.active_match_index, Some(2));
1418            search_bar.select_prev_match(&SelectPrevMatch, cx);
1419            assert_eq!(
1420                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1421                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1422            );
1423        });
1424        search_bar.update(cx, |search_bar, _| {
1425            assert_eq!(search_bar.active_match_index, Some(2));
1426        });
1427
1428        // Park the cursor after the last match and ensure that going to the next match selects the
1429        // first match.
1430        editor.update(cx, |editor, cx| {
1431            editor.change_selections(None, cx, |s| {
1432                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1433            });
1434        });
1435        search_bar.update(cx, |search_bar, cx| {
1436            assert_eq!(search_bar.active_match_index, Some(2));
1437            search_bar.select_next_match(&SelectNextMatch, cx);
1438            assert_eq!(
1439                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1440                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1441            );
1442        });
1443        search_bar.update(cx, |search_bar, _| {
1444            assert_eq!(search_bar.active_match_index, Some(0));
1445        });
1446
1447        // Park the cursor before the first match and ensure that going to the previous match
1448        // selects the last match.
1449        editor.update(cx, |editor, cx| {
1450            editor.change_selections(None, cx, |s| {
1451                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1452            });
1453        });
1454        search_bar.update(cx, |search_bar, cx| {
1455            assert_eq!(search_bar.active_match_index, Some(0));
1456            search_bar.select_prev_match(&SelectPrevMatch, cx);
1457            assert_eq!(
1458                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1459                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1460            );
1461        });
1462        search_bar.update(cx, |search_bar, _| {
1463            assert_eq!(search_bar.active_match_index, Some(2));
1464        });
1465    }
1466
1467    #[gpui::test]
1468    async fn test_search_option_handling(cx: &mut TestAppContext) {
1469        let (editor, search_bar, cx) = init_test(cx);
1470
1471        // show with options should make current search case sensitive
1472        search_bar
1473            .update(cx, |search_bar, cx| {
1474                search_bar.show(cx);
1475                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1476            })
1477            .await
1478            .unwrap();
1479        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1480            background_highlights
1481                .into_iter()
1482                .map(|(range, _)| range)
1483                .collect::<Vec<_>>()
1484        };
1485        editor.update(cx, |editor, cx| {
1486            assert_eq!(
1487                display_points_of(editor.all_text_background_highlights(cx)),
1488                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1489            );
1490        });
1491
1492        // search_suggested should restore default options
1493        search_bar.update(cx, |search_bar, cx| {
1494            search_bar.search_suggested(cx);
1495            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1496        });
1497
1498        // toggling a search option should update the defaults
1499        search_bar
1500            .update(cx, |search_bar, cx| {
1501                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1502            })
1503            .await
1504            .unwrap();
1505        search_bar.update(cx, |search_bar, cx| {
1506            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1507        });
1508        let mut editor_notifications = cx.notifications(&editor);
1509        editor_notifications.next().await;
1510        editor.update(cx, |editor, cx| {
1511            assert_eq!(
1512                display_points_of(editor.all_text_background_highlights(cx)),
1513                &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1514            );
1515        });
1516
1517        // defaults should still include whole word
1518        search_bar.update(cx, |search_bar, cx| {
1519            search_bar.search_suggested(cx);
1520            assert_eq!(
1521                search_bar.search_options,
1522                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1523            )
1524        });
1525    }
1526
1527    #[gpui::test]
1528    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1529        init_globals(cx);
1530        let buffer_text = r#"
1531        A regular expression (shortened as regex or regexp;[1] also referred to as
1532        rational expression[2][3]) is a sequence of characters that specifies a search
1533        pattern in text. Usually such patterns are used by string-searching algorithms
1534        for "find" or "find and replace" operations on strings, or for input validation.
1535        "#
1536        .unindent();
1537        let expected_query_matches_count = buffer_text
1538            .chars()
1539            .filter(|c| c.to_ascii_lowercase() == 'a')
1540            .count();
1541        assert!(
1542            expected_query_matches_count > 1,
1543            "Should pick a query with multiple results"
1544        );
1545        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1546        let window = cx.add_window(|_| EmptyView {});
1547
1548        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1549
1550        let search_bar = window.build_view(cx, |cx| {
1551            let mut search_bar = BufferSearchBar::new(cx);
1552            search_bar.set_active_pane_item(Some(&editor), cx);
1553            search_bar.show(cx);
1554            search_bar
1555        });
1556
1557        window
1558            .update(cx, |_, cx| {
1559                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1560            })
1561            .unwrap()
1562            .await
1563            .unwrap();
1564        let initial_selections = window
1565            .update(cx, |_, cx| {
1566                search_bar.update(cx, |search_bar, cx| {
1567                    let handle = search_bar.query_editor.focus_handle(cx);
1568                    cx.focus(&handle);
1569                    search_bar.activate_current_match(cx);
1570                });
1571                assert!(
1572                    !editor.read(cx).is_focused(cx),
1573                    "Initially, the editor should not be focused"
1574                );
1575                let initial_selections = editor.update(cx, |editor, cx| {
1576                    let initial_selections = editor.selections.display_ranges(cx);
1577                    assert_eq!(
1578                        initial_selections.len(), 1,
1579                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1580                    );
1581                    initial_selections
1582                });
1583                search_bar.update(cx, |search_bar, cx| {
1584                    assert_eq!(search_bar.active_match_index, Some(0));
1585                    let handle = search_bar.query_editor.focus_handle(cx);
1586                    cx.focus(&handle);
1587                    search_bar.select_all_matches(&SelectAllMatches, cx);
1588                });
1589                assert!(
1590                    editor.read(cx).is_focused(cx),
1591                    "Should focus editor after successful SelectAllMatches"
1592                );
1593                search_bar.update(cx, |search_bar, cx| {
1594                    let all_selections =
1595                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1596                    assert_eq!(
1597                        all_selections.len(),
1598                        expected_query_matches_count,
1599                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1600                    );
1601                    assert_eq!(
1602                        search_bar.active_match_index,
1603                        Some(0),
1604                        "Match index should not change after selecting all matches"
1605                    );
1606                });
1607
1608                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1609                initial_selections
1610            }).unwrap();
1611
1612        window
1613            .update(cx, |_, cx| {
1614                assert!(
1615                    editor.read(cx).is_focused(cx),
1616                    "Should still have editor focused after SelectNextMatch"
1617                );
1618                search_bar.update(cx, |search_bar, cx| {
1619                    let all_selections =
1620                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1621                    assert_eq!(
1622                        all_selections.len(),
1623                        1,
1624                        "On next match, should deselect items and select the next match"
1625                    );
1626                    assert_ne!(
1627                        all_selections, initial_selections,
1628                        "Next match should be different from the first selection"
1629                    );
1630                    assert_eq!(
1631                        search_bar.active_match_index,
1632                        Some(1),
1633                        "Match index should be updated to the next one"
1634                    );
1635                    let handle = search_bar.query_editor.focus_handle(cx);
1636                    cx.focus(&handle);
1637                    search_bar.select_all_matches(&SelectAllMatches, cx);
1638                });
1639            })
1640            .unwrap();
1641        window
1642            .update(cx, |_, cx| {
1643                assert!(
1644                    editor.read(cx).is_focused(cx),
1645                    "Should focus editor after successful SelectAllMatches"
1646                );
1647                search_bar.update(cx, |search_bar, cx| {
1648                    let all_selections =
1649                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1650                    assert_eq!(
1651                    all_selections.len(),
1652                    expected_query_matches_count,
1653                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1654                );
1655                    assert_eq!(
1656                        search_bar.active_match_index,
1657                        Some(1),
1658                        "Match index should not change after selecting all matches"
1659                    );
1660                });
1661                search_bar.update(cx, |search_bar, cx| {
1662                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1663                });
1664            })
1665            .unwrap();
1666        let last_match_selections = window
1667            .update(cx, |_, cx| {
1668                assert!(
1669                    editor.read(cx).is_focused(&cx),
1670                    "Should still have editor focused after SelectPrevMatch"
1671                );
1672
1673                search_bar.update(cx, |search_bar, cx| {
1674                    let all_selections =
1675                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1676                    assert_eq!(
1677                        all_selections.len(),
1678                        1,
1679                        "On previous match, should deselect items and select the previous item"
1680                    );
1681                    assert_eq!(
1682                        all_selections, initial_selections,
1683                        "Previous match should be the same as the first selection"
1684                    );
1685                    assert_eq!(
1686                        search_bar.active_match_index,
1687                        Some(0),
1688                        "Match index should be updated to the previous one"
1689                    );
1690                    all_selections
1691                })
1692            })
1693            .unwrap();
1694
1695        window
1696            .update(cx, |_, cx| {
1697                search_bar.update(cx, |search_bar, cx| {
1698                    let handle = search_bar.query_editor.focus_handle(cx);
1699                    cx.focus(&handle);
1700                    search_bar.search("abas_nonexistent_match", None, cx)
1701                })
1702            })
1703            .unwrap()
1704            .await
1705            .unwrap();
1706        window
1707            .update(cx, |_, cx| {
1708                search_bar.update(cx, |search_bar, cx| {
1709                    search_bar.select_all_matches(&SelectAllMatches, cx);
1710                });
1711                assert!(
1712                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1713                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1714                );
1715                search_bar.update(cx, |search_bar, cx| {
1716                    let all_selections =
1717                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1718                    assert_eq!(
1719                        all_selections, last_match_selections,
1720                        "Should not select anything new if there are no matches"
1721                    );
1722                    assert!(
1723                        search_bar.active_match_index.is_none(),
1724                        "For no matches, there should be no active match index"
1725                    );
1726                });
1727            })
1728            .unwrap();
1729    }
1730
1731    #[gpui::test]
1732    async fn test_search_query_history(cx: &mut TestAppContext) {
1733        init_globals(cx);
1734        let buffer_text = r#"
1735        A regular expression (shortened as regex or regexp;[1] also referred to as
1736        rational expression[2][3]) is a sequence of characters that specifies a search
1737        pattern in text. Usually such patterns are used by string-searching algorithms
1738        for "find" or "find and replace" operations on strings, or for input validation.
1739        "#
1740        .unindent();
1741        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1742        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1743
1744        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1745
1746        let search_bar = cx.new_view(|cx| {
1747            let mut search_bar = BufferSearchBar::new(cx);
1748            search_bar.set_active_pane_item(Some(&editor), cx);
1749            search_bar.show(cx);
1750            search_bar
1751        });
1752
1753        // Add 3 search items into the history.
1754        search_bar
1755            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1756            .await
1757            .unwrap();
1758        search_bar
1759            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1760            .await
1761            .unwrap();
1762        search_bar
1763            .update(cx, |search_bar, cx| {
1764                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1765            })
1766            .await
1767            .unwrap();
1768        // Ensure that the latest search is active.
1769        search_bar.update(cx, |search_bar, cx| {
1770            assert_eq!(search_bar.query(cx), "c");
1771            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1772        });
1773
1774        // Next history query after the latest should set the query to the empty string.
1775        search_bar.update(cx, |search_bar, cx| {
1776            search_bar.next_history_query(&NextHistoryQuery, cx);
1777        });
1778        search_bar.update(cx, |search_bar, cx| {
1779            assert_eq!(search_bar.query(cx), "");
1780            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1781        });
1782        search_bar.update(cx, |search_bar, cx| {
1783            search_bar.next_history_query(&NextHistoryQuery, cx);
1784        });
1785        search_bar.update(cx, |search_bar, cx| {
1786            assert_eq!(search_bar.query(cx), "");
1787            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1788        });
1789
1790        // First previous query for empty current query should set the query to the latest.
1791        search_bar.update(cx, |search_bar, cx| {
1792            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1793        });
1794        search_bar.update(cx, |search_bar, cx| {
1795            assert_eq!(search_bar.query(cx), "c");
1796            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1797        });
1798
1799        // Further previous items should go over the history in reverse order.
1800        search_bar.update(cx, |search_bar, cx| {
1801            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1802        });
1803        search_bar.update(cx, |search_bar, cx| {
1804            assert_eq!(search_bar.query(cx), "b");
1805            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1806        });
1807
1808        // Previous items should never go behind the first history item.
1809        search_bar.update(cx, |search_bar, cx| {
1810            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1811        });
1812        search_bar.update(cx, |search_bar, cx| {
1813            assert_eq!(search_bar.query(cx), "a");
1814            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1815        });
1816        search_bar.update(cx, |search_bar, cx| {
1817            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1818        });
1819        search_bar.update(cx, |search_bar, cx| {
1820            assert_eq!(search_bar.query(cx), "a");
1821            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1822        });
1823
1824        // Next items should go over the history in the original order.
1825        search_bar.update(cx, |search_bar, cx| {
1826            search_bar.next_history_query(&NextHistoryQuery, cx);
1827        });
1828        search_bar.update(cx, |search_bar, cx| {
1829            assert_eq!(search_bar.query(cx), "b");
1830            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1831        });
1832
1833        search_bar
1834            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1835            .await
1836            .unwrap();
1837        search_bar.update(cx, |search_bar, cx| {
1838            assert_eq!(search_bar.query(cx), "ba");
1839            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1840        });
1841
1842        // New search input should add another entry to history and move the selection to the end of the history.
1843        search_bar.update(cx, |search_bar, cx| {
1844            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1845        });
1846        search_bar.update(cx, |search_bar, cx| {
1847            assert_eq!(search_bar.query(cx), "c");
1848            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1849        });
1850        search_bar.update(cx, |search_bar, cx| {
1851            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1852        });
1853        search_bar.update(cx, |search_bar, cx| {
1854            assert_eq!(search_bar.query(cx), "b");
1855            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1856        });
1857        search_bar.update(cx, |search_bar, cx| {
1858            search_bar.next_history_query(&NextHistoryQuery, cx);
1859        });
1860        search_bar.update(cx, |search_bar, cx| {
1861            assert_eq!(search_bar.query(cx), "c");
1862            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1863        });
1864        search_bar.update(cx, |search_bar, cx| {
1865            search_bar.next_history_query(&NextHistoryQuery, cx);
1866        });
1867        search_bar.update(cx, |search_bar, cx| {
1868            assert_eq!(search_bar.query(cx), "ba");
1869            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1870        });
1871        search_bar.update(cx, |search_bar, cx| {
1872            search_bar.next_history_query(&NextHistoryQuery, cx);
1873        });
1874        search_bar.update(cx, |search_bar, cx| {
1875            assert_eq!(search_bar.query(cx), "");
1876            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1877        });
1878    }
1879
1880    #[gpui::test]
1881    async fn test_replace_simple(cx: &mut TestAppContext) {
1882        let (editor, search_bar, cx) = init_test(cx);
1883
1884        search_bar
1885            .update(cx, |search_bar, cx| {
1886                search_bar.search("expression", None, cx)
1887            })
1888            .await
1889            .unwrap();
1890
1891        search_bar.update(cx, |search_bar, cx| {
1892            search_bar.replacement_editor.update(cx, |editor, cx| {
1893                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1894                editor.set_text("expr$1", cx);
1895            });
1896            search_bar.replace_all(&ReplaceAll, cx)
1897        });
1898        assert_eq!(
1899            editor.update(cx, |this, cx| { this.text(cx) }),
1900            r#"
1901        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1902        rational expr$1[2][3]) is a sequence of characters that specifies a search
1903        pattern in text. Usually such patterns are used by string-searching algorithms
1904        for "find" or "find and replace" operations on strings, or for input validation.
1905        "#
1906            .unindent()
1907        );
1908
1909        // Search for word boundaries and replace just a single one.
1910        search_bar
1911            .update(cx, |search_bar, cx| {
1912                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1913            })
1914            .await
1915            .unwrap();
1916
1917        search_bar.update(cx, |search_bar, cx| {
1918            search_bar.replacement_editor.update(cx, |editor, cx| {
1919                editor.set_text("banana", cx);
1920            });
1921            search_bar.replace_next(&ReplaceNext, cx)
1922        });
1923        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1924        assert_eq!(
1925            editor.update(cx, |this, cx| { this.text(cx) }),
1926            r#"
1927        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1928        rational expr$1[2][3]) is a sequence of characters that specifies a search
1929        pattern in text. Usually such patterns are used by string-searching algorithms
1930        for "find" or "find and replace" operations on strings, or for input validation.
1931        "#
1932            .unindent()
1933        );
1934        // Let's turn on regex mode.
1935        search_bar
1936            .update(cx, |search_bar, cx| {
1937                search_bar.activate_search_mode(SearchMode::Regex, cx);
1938                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1939            })
1940            .await
1941            .unwrap();
1942        search_bar.update(cx, |search_bar, cx| {
1943            search_bar.replacement_editor.update(cx, |editor, cx| {
1944                editor.set_text("${1}number", cx);
1945            });
1946            search_bar.replace_all(&ReplaceAll, cx)
1947        });
1948        assert_eq!(
1949            editor.update(cx, |this, cx| { this.text(cx) }),
1950            r#"
1951        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1952        rational expr$12number3number) is a sequence of characters that specifies a search
1953        pattern in text. Usually such patterns are used by string-searching algorithms
1954        for "find" or "find and replace" operations on strings, or for input validation.
1955        "#
1956            .unindent()
1957        );
1958        // Now with a whole-word twist.
1959        search_bar
1960            .update(cx, |search_bar, cx| {
1961                search_bar.activate_search_mode(SearchMode::Regex, cx);
1962                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1963            })
1964            .await
1965            .unwrap();
1966        search_bar.update(cx, |search_bar, cx| {
1967            search_bar.replacement_editor.update(cx, |editor, cx| {
1968                editor.set_text("things", cx);
1969            });
1970            search_bar.replace_all(&ReplaceAll, cx)
1971        });
1972        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1973        // of words in this text that would match this regex if not for WHOLE_WORD.
1974        assert_eq!(
1975            editor.update(cx, |this, cx| { this.text(cx) }),
1976            r#"
1977        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1978        rational expr$12number3number) is a sequence of characters that specifies a search
1979        pattern in text. Usually such patterns are used by string-searching things
1980        for "find" or "find and replace" operations on strings, or for input validation.
1981        "#
1982            .unindent()
1983        );
1984    }
1985}