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_stack, prelude::*, Icon, IconButton, IconElement, 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(|editor: &mut Workspace, _| BufferSearchBar::register(editor))
  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_stack()
 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_stack()
 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(IconElement::new(Icon::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_stack()
 247                    .gap_2()
 248                    .flex_none()
 249                    .child(
 250                        h_stack()
 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                                Icon::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_stack()
 307                    .gap_0p5()
 308                    .flex_1()
 309                    .when(self.replace_enabled, |this| {
 310                        this.child(
 311                            h_stack()
 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::Icon::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::Icon::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_stack()
 350                    .gap_0p5()
 351                    .flex_none()
 352                    .child(
 353                        IconButton::new("select-all", ui::Icon::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::Icon::ChevronLeft,
 362                        self.active_match_index.is_some(),
 363                        "Select previous match",
 364                        &SelectPrevMatch,
 365                    ))
 366                    .child(render_nav_button(
 367                        ui::Icon::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 it's 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
 434type GetSearchBar<T> =
 435    for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
 436
 437/// Registers search actions on a div that can be taken out.
 438pub struct DivRegistrar<'a, 'b, T: 'static> {
 439    div: Option<Div>,
 440    cx: &'a mut ViewContext<'b, T>,
 441    search_getter: GetSearchBar<T>,
 442}
 443
 444impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
 445    pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
 446        Self {
 447            div: Some(div()),
 448            cx,
 449            search_getter,
 450        }
 451    }
 452    pub fn into_div(self) -> Div {
 453        // This option is always Some; it's an option in the first place because we want to call methods
 454        // on div that require ownership.
 455        self.div.unwrap()
 456    }
 457}
 458
 459impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
 460    fn register_handler<A: gpui::Action>(
 461        &mut self,
 462        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 463    ) {
 464        let getter = self.search_getter;
 465        self.div = self.div.take().map(|div| {
 466            div.on_action(self.cx.listener(move |this, action, cx| {
 467                (getter)(this, cx)
 468                    .clone()
 469                    .map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx)));
 470            }))
 471        });
 472    }
 473}
 474
 475/// Register actions for an active pane.
 476impl SearchActionsRegistrar for Workspace {
 477    fn register_handler<A: Action>(
 478        &mut self,
 479        callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 480    ) {
 481        self.register_action(move |workspace, action: &A, cx| {
 482            let pane = workspace.active_pane();
 483            pane.update(cx, move |this, cx| {
 484                this.toolbar().update(cx, move |this, cx| {
 485                    if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
 486                        search_bar.update(cx, move |this, cx| callback(this, action, cx));
 487                        cx.notify();
 488                    }
 489                })
 490            });
 491        });
 492    }
 493}
 494impl BufferSearchBar {
 495    pub fn register_inner(registrar: &mut impl SearchActionsRegistrar) {
 496        registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
 497            if this.supported_options().case {
 498                this.toggle_case_sensitive(action, cx);
 499            }
 500        });
 501
 502        registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
 503            if this.supported_options().word {
 504                this.toggle_whole_word(action, cx);
 505            }
 506        });
 507
 508        registrar.register_handler(|this, action: &ToggleReplace, cx| {
 509            if this.supported_options().replacement {
 510                this.toggle_replace(action, cx);
 511            }
 512        });
 513
 514        registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
 515            if this.supported_options().regex {
 516                this.activate_search_mode(SearchMode::Regex, cx);
 517            }
 518        });
 519
 520        registrar.register_handler(|this, _: &ActivateTextMode, cx| {
 521            this.activate_search_mode(SearchMode::Text, cx);
 522        });
 523
 524        registrar.register_handler(|this, action: &CycleMode, cx| {
 525            if this.supported_options().regex {
 526                // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
 527                // cycling.
 528                this.cycle_mode(action, cx)
 529            }
 530        });
 531
 532        registrar.register_handler(|this, action: &SelectNextMatch, cx| {
 533            this.select_next_match(action, cx);
 534        });
 535        registrar.register_handler(|this, action: &SelectPrevMatch, cx| {
 536            this.select_prev_match(action, cx);
 537        });
 538        registrar.register_handler(|this, action: &SelectAllMatches, cx| {
 539            this.select_all_matches(action, cx);
 540        });
 541        registrar.register_handler(|this, _: &editor::Cancel, cx| {
 542            if !this.dismissed {
 543                this.dismiss(&Dismiss, cx);
 544                return;
 545            }
 546            cx.propagate();
 547        });
 548        registrar.register_handler(|this, deploy, cx| {
 549            this.deploy(deploy, cx);
 550        })
 551    }
 552    fn register(workspace: &mut Workspace) {
 553        Self::register_inner(workspace);
 554    }
 555    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 556        let query_editor = cx.new_view(|cx| Editor::single_line(cx));
 557        cx.subscribe(&query_editor, Self::on_query_editor_event)
 558            .detach();
 559        let replacement_editor = cx.new_view(|cx| Editor::single_line(cx));
 560        cx.subscribe(&replacement_editor, Self::on_query_editor_event)
 561            .detach();
 562        Self {
 563            query_editor,
 564            replacement_editor,
 565            active_searchable_item: None,
 566            active_searchable_item_subscription: None,
 567            active_match_index: None,
 568            searchable_items_with_matches: Default::default(),
 569            default_options: SearchOptions::NONE,
 570            search_options: SearchOptions::NONE,
 571            pending_search: None,
 572            query_contains_error: false,
 573            dismissed: true,
 574            search_history: SearchHistory::default(),
 575            current_mode: SearchMode::default(),
 576            active_search: None,
 577            replace_enabled: false,
 578        }
 579    }
 580
 581    pub fn is_dismissed(&self) -> bool {
 582        self.dismissed
 583    }
 584
 585    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 586        self.dismissed = true;
 587        for searchable_item in self.searchable_items_with_matches.keys() {
 588            if let Some(searchable_item) =
 589                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 590            {
 591                searchable_item.clear_matches(cx);
 592            }
 593        }
 594        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 595            let handle = active_editor.focus_handle(cx);
 596            cx.focus(&handle);
 597        }
 598        cx.emit(Event::UpdateLocation);
 599        cx.emit(ToolbarItemEvent::ChangeLocation(
 600            ToolbarItemLocation::Hidden,
 601        ));
 602        cx.notify();
 603    }
 604
 605    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 606        if self.show(cx) {
 607            self.search_suggested(cx);
 608            if deploy.focus {
 609                self.select_query(cx);
 610                let handle = self.query_editor.focus_handle(cx);
 611                cx.focus(&handle);
 612            }
 613            return true;
 614        }
 615
 616        false
 617    }
 618
 619    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
 620        if self.is_dismissed() {
 621            self.deploy(action, cx);
 622        } else {
 623            self.dismiss(&Dismiss, cx);
 624        }
 625    }
 626
 627    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 628        if self.active_searchable_item.is_none() {
 629            return false;
 630        }
 631        self.dismissed = false;
 632        cx.notify();
 633        cx.emit(Event::UpdateLocation);
 634        cx.emit(ToolbarItemEvent::ChangeLocation(
 635            ToolbarItemLocation::Secondary,
 636        ));
 637        true
 638    }
 639
 640    fn supported_options(&self) -> workspace::searchable::SearchOptions {
 641        self.active_searchable_item
 642            .as_deref()
 643            .map(SearchableItemHandle::supported_options)
 644            .unwrap_or_default()
 645    }
 646    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 647        let search = self
 648            .query_suggestion(cx)
 649            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 650
 651        if let Some(search) = search {
 652            cx.spawn(|this, mut cx| async move {
 653                search.await?;
 654                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 655            })
 656            .detach_and_log_err(cx);
 657        }
 658    }
 659
 660    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 661        if let Some(match_ix) = self.active_match_index {
 662            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 663                if let Some(matches) = self
 664                    .searchable_items_with_matches
 665                    .get(&active_searchable_item.downgrade())
 666                {
 667                    active_searchable_item.activate_match(match_ix, matches, cx)
 668                }
 669            }
 670        }
 671    }
 672
 673    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 674        self.query_editor.update(cx, |query_editor, cx| {
 675            query_editor.select_all(&Default::default(), cx);
 676        });
 677    }
 678
 679    pub fn query(&self, cx: &WindowContext) -> String {
 680        self.query_editor.read(cx).text(cx)
 681    }
 682    pub fn replacement(&self, cx: &WindowContext) -> String {
 683        self.replacement_editor.read(cx).text(cx)
 684    }
 685    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 686        self.active_searchable_item
 687            .as_ref()
 688            .map(|searchable_item| searchable_item.query_suggestion(cx))
 689            .filter(|suggestion| !suggestion.is_empty())
 690    }
 691
 692    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 693        if replacement.is_none() {
 694            self.replace_enabled = false;
 695            return;
 696        }
 697        self.replace_enabled = true;
 698        self.replacement_editor
 699            .update(cx, |replacement_editor, cx| {
 700                replacement_editor
 701                    .buffer()
 702                    .update(cx, |replacement_buffer, cx| {
 703                        let len = replacement_buffer.len(cx);
 704                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 705                    });
 706            });
 707    }
 708
 709    pub fn search(
 710        &mut self,
 711        query: &str,
 712        options: Option<SearchOptions>,
 713        cx: &mut ViewContext<Self>,
 714    ) -> oneshot::Receiver<()> {
 715        let options = options.unwrap_or(self.default_options);
 716        if query != self.query(cx) || self.search_options != options {
 717            self.query_editor.update(cx, |query_editor, cx| {
 718                query_editor.buffer().update(cx, |query_buffer, cx| {
 719                    let len = query_buffer.len(cx);
 720                    query_buffer.edit([(0..len, query)], None, cx);
 721                });
 722            });
 723            self.search_options = options;
 724            self.query_contains_error = false;
 725            self.clear_matches(cx);
 726            cx.notify();
 727        }
 728        self.update_matches(cx)
 729    }
 730
 731    fn render_search_option_button(
 732        &self,
 733        option: SearchOptions,
 734        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 735    ) -> impl IntoElement {
 736        let is_active = self.search_options.contains(option);
 737        option.as_button(is_active, action)
 738    }
 739    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 740        assert_ne!(
 741            mode,
 742            SearchMode::Semantic,
 743            "Semantic search is not supported in buffer search"
 744        );
 745        if mode == self.current_mode {
 746            return;
 747        }
 748        self.current_mode = mode;
 749        let _ = self.update_matches(cx);
 750        cx.notify();
 751    }
 752
 753    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 754        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 755            let handle = active_editor.focus_handle(cx);
 756            cx.focus(&handle);
 757        }
 758    }
 759
 760    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 761        self.search_options.toggle(search_option);
 762        self.default_options = self.search_options;
 763        let _ = self.update_matches(cx);
 764        cx.notify();
 765    }
 766
 767    pub fn set_search_options(
 768        &mut self,
 769        search_options: SearchOptions,
 770        cx: &mut ViewContext<Self>,
 771    ) {
 772        self.search_options = search_options;
 773        cx.notify();
 774    }
 775
 776    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 777        self.select_match(Direction::Next, 1, cx);
 778    }
 779
 780    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 781        self.select_match(Direction::Prev, 1, cx);
 782    }
 783
 784    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 785        if !self.dismissed && self.active_match_index.is_some() {
 786            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 787                if let Some(matches) = self
 788                    .searchable_items_with_matches
 789                    .get(&searchable_item.downgrade())
 790                {
 791                    searchable_item.select_matches(matches, cx);
 792                    self.focus_editor(&FocusEditor, cx);
 793                }
 794            }
 795        }
 796    }
 797
 798    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 799        if let Some(index) = self.active_match_index {
 800            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 801                if let Some(matches) = self
 802                    .searchable_items_with_matches
 803                    .get(&searchable_item.downgrade())
 804                {
 805                    let new_match_index = searchable_item
 806                        .match_index_for_direction(matches, index, direction, count, cx);
 807
 808                    searchable_item.update_matches(matches, cx);
 809                    searchable_item.activate_match(new_match_index, matches, cx);
 810                }
 811            }
 812        }
 813    }
 814
 815    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 816        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 817            if let Some(matches) = self
 818                .searchable_items_with_matches
 819                .get(&searchable_item.downgrade())
 820            {
 821                if matches.len() == 0 {
 822                    return;
 823                }
 824                let new_match_index = matches.len() - 1;
 825                searchable_item.update_matches(matches, cx);
 826                searchable_item.activate_match(new_match_index, matches, cx);
 827            }
 828        }
 829    }
 830
 831    fn on_query_editor_event(
 832        &mut self,
 833        _: View<Editor>,
 834        event: &editor::EditorEvent,
 835        cx: &mut ViewContext<Self>,
 836    ) {
 837        if let editor::EditorEvent::Edited { .. } = event {
 838            self.query_contains_error = false;
 839            self.clear_matches(cx);
 840            let search = self.update_matches(cx);
 841            cx.spawn(|this, mut cx| async move {
 842                search.await?;
 843                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 844            })
 845            .detach_and_log_err(cx);
 846        }
 847    }
 848
 849    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 850        match event {
 851            SearchEvent::MatchesInvalidated => {
 852                let _ = self.update_matches(cx);
 853            }
 854            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 855        }
 856    }
 857
 858    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 859        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 860    }
 861    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 862        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 863    }
 864    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 865        let mut active_item_matches = None;
 866        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 867            if let Some(searchable_item) =
 868                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 869            {
 870                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 871                    active_item_matches = Some((searchable_item.downgrade(), matches));
 872                } else {
 873                    searchable_item.clear_matches(cx);
 874                }
 875            }
 876        }
 877
 878        self.searchable_items_with_matches
 879            .extend(active_item_matches);
 880    }
 881
 882    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 883        let (done_tx, done_rx) = oneshot::channel();
 884        let query = self.query(cx);
 885        self.pending_search.take();
 886
 887        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 888            if query.is_empty() {
 889                self.active_match_index.take();
 890                active_searchable_item.clear_matches(cx);
 891                let _ = done_tx.send(());
 892                cx.notify();
 893            } else {
 894                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
 895                    match SearchQuery::regex(
 896                        query,
 897                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 898                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 899                        false,
 900                        Vec::new(),
 901                        Vec::new(),
 902                    ) {
 903                        Ok(query) => query.with_replacement(self.replacement(cx)),
 904                        Err(_) => {
 905                            self.query_contains_error = true;
 906                            self.active_match_index = None;
 907                            cx.notify();
 908                            return done_rx;
 909                        }
 910                    }
 911                } else {
 912                    match SearchQuery::text(
 913                        query,
 914                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 915                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 916                        false,
 917                        Vec::new(),
 918                        Vec::new(),
 919                    ) {
 920                        Ok(query) => query.with_replacement(self.replacement(cx)),
 921                        Err(_) => {
 922                            self.query_contains_error = true;
 923                            self.active_match_index = None;
 924                            cx.notify();
 925                            return done_rx;
 926                        }
 927                    }
 928                }
 929                .into();
 930                self.active_search = Some(query.clone());
 931                let query_text = query.as_str().to_string();
 932
 933                let matches = active_searchable_item.find_matches(query, cx);
 934
 935                let active_searchable_item = active_searchable_item.downgrade();
 936                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 937                    let matches = matches.await;
 938
 939                    this.update(&mut cx, |this, cx| {
 940                        if let Some(active_searchable_item) =
 941                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 942                        {
 943                            this.searchable_items_with_matches
 944                                .insert(active_searchable_item.downgrade(), matches);
 945
 946                            this.update_match_index(cx);
 947                            this.search_history.add(query_text);
 948                            if !this.dismissed {
 949                                let matches = this
 950                                    .searchable_items_with_matches
 951                                    .get(&active_searchable_item.downgrade())
 952                                    .unwrap();
 953                                active_searchable_item.update_matches(matches, cx);
 954                                let _ = done_tx.send(());
 955                            }
 956                            cx.notify();
 957                        }
 958                    })
 959                    .log_err();
 960                }));
 961            }
 962        }
 963        done_rx
 964    }
 965
 966    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 967        let new_index = self
 968            .active_searchable_item
 969            .as_ref()
 970            .and_then(|searchable_item| {
 971                let matches = self
 972                    .searchable_items_with_matches
 973                    .get(&searchable_item.downgrade())?;
 974                searchable_item.active_match_index(matches, cx)
 975            });
 976        if new_index != self.active_match_index {
 977            self.active_match_index = new_index;
 978            cx.notify();
 979        }
 980    }
 981
 982    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
 983        if let Some(item) = self.active_searchable_item.as_ref() {
 984            let focus_handle = item.focus_handle(cx);
 985            cx.focus(&focus_handle);
 986            cx.stop_propagation();
 987        }
 988    }
 989
 990    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
 991        if let Some(new_query) = self.search_history.next().map(str::to_string) {
 992            let _ = self.search(&new_query, Some(self.search_options), cx);
 993        } else {
 994            self.search_history.reset_selection();
 995            let _ = self.search("", Some(self.search_options), cx);
 996        }
 997    }
 998
 999    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1000        if self.query(cx).is_empty() {
1001            if let Some(new_query) = self.search_history.current().map(str::to_string) {
1002                let _ = self.search(&new_query, Some(self.search_options), cx);
1003                return;
1004            }
1005        }
1006
1007        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
1008            let _ = self.search(&new_query, Some(self.search_options), cx);
1009        }
1010    }
1011    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
1012        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
1013    }
1014    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
1015        if let Some(_) = &self.active_searchable_item {
1016            self.replace_enabled = !self.replace_enabled;
1017            if !self.replace_enabled {
1018                let handle = self.query_editor.focus_handle(cx);
1019                cx.focus(&handle);
1020            }
1021            cx.notify();
1022        }
1023    }
1024    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
1025        let mut should_propagate = true;
1026        if !self.dismissed && self.active_search.is_some() {
1027            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1028                if let Some(query) = self.active_search.as_ref() {
1029                    if let Some(matches) = self
1030                        .searchable_items_with_matches
1031                        .get(&searchable_item.downgrade())
1032                    {
1033                        if let Some(active_index) = self.active_match_index {
1034                            let query = query
1035                                .as_ref()
1036                                .clone()
1037                                .with_replacement(self.replacement(cx));
1038                            searchable_item.replace(&matches[active_index], &query, cx);
1039                            self.select_next_match(&SelectNextMatch, cx);
1040                        }
1041                        should_propagate = false;
1042                        self.focus_editor(&FocusEditor, cx);
1043                    }
1044                }
1045            }
1046        }
1047        if !should_propagate {
1048            cx.stop_propagation();
1049        }
1050    }
1051    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1052        if !self.dismissed && self.active_search.is_some() {
1053            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1054                if let Some(query) = self.active_search.as_ref() {
1055                    if let Some(matches) = self
1056                        .searchable_items_with_matches
1057                        .get(&searchable_item.downgrade())
1058                    {
1059                        let query = query
1060                            .as_ref()
1061                            .clone()
1062                            .with_replacement(self.replacement(cx));
1063                        for m in matches {
1064                            searchable_item.replace(m, &query, cx);
1065                        }
1066                    }
1067                }
1068            }
1069        }
1070    }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    use std::ops::Range;
1076
1077    use super::*;
1078    use editor::{DisplayPoint, Editor};
1079    use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
1080    use language::Buffer;
1081    use smol::stream::StreamExt as _;
1082    use unindent::Unindent as _;
1083
1084    fn init_globals(cx: &mut TestAppContext) {
1085        cx.update(|cx| {
1086            let store = settings::SettingsStore::test(cx);
1087            cx.set_global(store);
1088            editor::init(cx);
1089
1090            language::init(cx);
1091            theme::init(theme::LoadThemes::JustBase, cx);
1092        });
1093    }
1094    fn init_test(
1095        cx: &mut TestAppContext,
1096    ) -> (
1097        View<Editor>,
1098        View<BufferSearchBar>,
1099        &mut VisualTestContext<'_>,
1100    ) {
1101        init_globals(cx);
1102        let buffer = cx.new_model(|cx| {
1103            Buffer::new(
1104                0,
1105                cx.entity_id().as_u64(),
1106                r#"
1107                A regular expression (shortened as regex or regexp;[1] also referred to as
1108                rational expression[2][3]) is a sequence of characters that specifies a search
1109                pattern in text. Usually such patterns are used by string-searching algorithms
1110                for "find" or "find and replace" operations on strings, or for input validation.
1111                "#
1112                .unindent(),
1113            )
1114        });
1115        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1116        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1117
1118        let search_bar = cx.new_view(|cx| {
1119            let mut search_bar = BufferSearchBar::new(cx);
1120            search_bar.set_active_pane_item(Some(&editor), cx);
1121            search_bar.show(cx);
1122            search_bar
1123        });
1124
1125        (editor, search_bar, cx)
1126    }
1127
1128    #[gpui::test]
1129    async fn test_search_simple(cx: &mut TestAppContext) {
1130        let (editor, search_bar, cx) = init_test(cx);
1131        // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
1132        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1133            background_highlights
1134                .into_iter()
1135                .map(|(range, _)| range)
1136                .collect::<Vec<_>>()
1137        };
1138        // Search for a string that appears with different casing.
1139        // By default, search is case-insensitive.
1140        search_bar
1141            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1142            .await
1143            .unwrap();
1144        editor.update(cx, |editor, cx| {
1145            assert_eq!(
1146                display_points_of(editor.all_text_background_highlights(cx)),
1147                &[
1148                    DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1149                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1150                ]
1151            );
1152        });
1153
1154        // Switch to a case sensitive search.
1155        search_bar.update(cx, |search_bar, cx| {
1156            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1157        });
1158        let mut editor_notifications = cx.notifications(&editor);
1159        editor_notifications.next().await;
1160        editor.update(cx, |editor, cx| {
1161            assert_eq!(
1162                display_points_of(editor.all_text_background_highlights(cx)),
1163                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1164            );
1165        });
1166
1167        // Search for a string that appears both as a whole word and
1168        // within other words. By default, all results are found.
1169        search_bar
1170            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1171            .await
1172            .unwrap();
1173        editor.update(cx, |editor, cx| {
1174            assert_eq!(
1175                display_points_of(editor.all_text_background_highlights(cx)),
1176                &[
1177                    DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1178                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1179                    DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1180                    DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1181                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1182                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1183                    DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1184                ]
1185            );
1186        });
1187
1188        // Switch to a whole word search.
1189        search_bar.update(cx, |search_bar, cx| {
1190            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1191        });
1192        let mut editor_notifications = cx.notifications(&editor);
1193        editor_notifications.next().await;
1194        editor.update(cx, |editor, cx| {
1195            assert_eq!(
1196                display_points_of(editor.all_text_background_highlights(cx)),
1197                &[
1198                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1199                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1200                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1201                ]
1202            );
1203        });
1204
1205        editor.update(cx, |editor, cx| {
1206            editor.change_selections(None, cx, |s| {
1207                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1208            });
1209        });
1210        search_bar.update(cx, |search_bar, cx| {
1211            assert_eq!(search_bar.active_match_index, Some(0));
1212            search_bar.select_next_match(&SelectNextMatch, cx);
1213            assert_eq!(
1214                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1215                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1216            );
1217        });
1218        search_bar.update(cx, |search_bar, _| {
1219            assert_eq!(search_bar.active_match_index, Some(0));
1220        });
1221
1222        search_bar.update(cx, |search_bar, cx| {
1223            search_bar.select_next_match(&SelectNextMatch, cx);
1224            assert_eq!(
1225                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1226                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1227            );
1228        });
1229        search_bar.update(cx, |search_bar, _| {
1230            assert_eq!(search_bar.active_match_index, Some(1));
1231        });
1232
1233        search_bar.update(cx, |search_bar, cx| {
1234            search_bar.select_next_match(&SelectNextMatch, cx);
1235            assert_eq!(
1236                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1237                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1238            );
1239        });
1240        search_bar.update(cx, |search_bar, _| {
1241            assert_eq!(search_bar.active_match_index, Some(2));
1242        });
1243
1244        search_bar.update(cx, |search_bar, cx| {
1245            search_bar.select_next_match(&SelectNextMatch, cx);
1246            assert_eq!(
1247                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1248                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1249            );
1250        });
1251        search_bar.update(cx, |search_bar, _| {
1252            assert_eq!(search_bar.active_match_index, Some(0));
1253        });
1254
1255        search_bar.update(cx, |search_bar, cx| {
1256            search_bar.select_prev_match(&SelectPrevMatch, cx);
1257            assert_eq!(
1258                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1259                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1260            );
1261        });
1262        search_bar.update(cx, |search_bar, _| {
1263            assert_eq!(search_bar.active_match_index, Some(2));
1264        });
1265
1266        search_bar.update(cx, |search_bar, cx| {
1267            search_bar.select_prev_match(&SelectPrevMatch, cx);
1268            assert_eq!(
1269                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1270                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1271            );
1272        });
1273        search_bar.update(cx, |search_bar, _| {
1274            assert_eq!(search_bar.active_match_index, Some(1));
1275        });
1276
1277        search_bar.update(cx, |search_bar, cx| {
1278            search_bar.select_prev_match(&SelectPrevMatch, cx);
1279            assert_eq!(
1280                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1281                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1282            );
1283        });
1284        search_bar.update(cx, |search_bar, _| {
1285            assert_eq!(search_bar.active_match_index, Some(0));
1286        });
1287
1288        // Park the cursor in between matches and ensure that going to the previous match selects
1289        // the closest match to the left.
1290        editor.update(cx, |editor, cx| {
1291            editor.change_selections(None, cx, |s| {
1292                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1293            });
1294        });
1295        search_bar.update(cx, |search_bar, cx| {
1296            assert_eq!(search_bar.active_match_index, Some(1));
1297            search_bar.select_prev_match(&SelectPrevMatch, cx);
1298            assert_eq!(
1299                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1300                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1301            );
1302        });
1303        search_bar.update(cx, |search_bar, _| {
1304            assert_eq!(search_bar.active_match_index, Some(0));
1305        });
1306
1307        // Park the cursor in between matches and ensure that going to the next match selects the
1308        // closest match to the right.
1309        editor.update(cx, |editor, cx| {
1310            editor.change_selections(None, cx, |s| {
1311                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1312            });
1313        });
1314        search_bar.update(cx, |search_bar, cx| {
1315            assert_eq!(search_bar.active_match_index, Some(1));
1316            search_bar.select_next_match(&SelectNextMatch, cx);
1317            assert_eq!(
1318                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1319                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1320            );
1321        });
1322        search_bar.update(cx, |search_bar, _| {
1323            assert_eq!(search_bar.active_match_index, Some(1));
1324        });
1325
1326        // Park the cursor after the last match and ensure that going to the previous match selects
1327        // the last match.
1328        editor.update(cx, |editor, cx| {
1329            editor.change_selections(None, cx, |s| {
1330                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1331            });
1332        });
1333        search_bar.update(cx, |search_bar, cx| {
1334            assert_eq!(search_bar.active_match_index, Some(2));
1335            search_bar.select_prev_match(&SelectPrevMatch, cx);
1336            assert_eq!(
1337                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1338                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1339            );
1340        });
1341        search_bar.update(cx, |search_bar, _| {
1342            assert_eq!(search_bar.active_match_index, Some(2));
1343        });
1344
1345        // Park the cursor after the last match and ensure that going to the next match selects the
1346        // first match.
1347        editor.update(cx, |editor, cx| {
1348            editor.change_selections(None, cx, |s| {
1349                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1350            });
1351        });
1352        search_bar.update(cx, |search_bar, cx| {
1353            assert_eq!(search_bar.active_match_index, Some(2));
1354            search_bar.select_next_match(&SelectNextMatch, cx);
1355            assert_eq!(
1356                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1357                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1358            );
1359        });
1360        search_bar.update(cx, |search_bar, _| {
1361            assert_eq!(search_bar.active_match_index, Some(0));
1362        });
1363
1364        // Park the cursor before the first match and ensure that going to the previous match
1365        // selects the last match.
1366        editor.update(cx, |editor, cx| {
1367            editor.change_selections(None, cx, |s| {
1368                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1369            });
1370        });
1371        search_bar.update(cx, |search_bar, cx| {
1372            assert_eq!(search_bar.active_match_index, Some(0));
1373            search_bar.select_prev_match(&SelectPrevMatch, cx);
1374            assert_eq!(
1375                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1376                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1377            );
1378        });
1379        search_bar.update(cx, |search_bar, _| {
1380            assert_eq!(search_bar.active_match_index, Some(2));
1381        });
1382    }
1383
1384    #[gpui::test]
1385    async fn test_search_option_handling(cx: &mut TestAppContext) {
1386        let (editor, search_bar, cx) = init_test(cx);
1387
1388        // show with options should make current search case sensitive
1389        search_bar
1390            .update(cx, |search_bar, cx| {
1391                search_bar.show(cx);
1392                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1393            })
1394            .await
1395            .unwrap();
1396        // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1397        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1398            background_highlights
1399                .into_iter()
1400                .map(|(range, _)| range)
1401                .collect::<Vec<_>>()
1402        };
1403        editor.update(cx, |editor, cx| {
1404            assert_eq!(
1405                display_points_of(editor.all_text_background_highlights(cx)),
1406                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1407            );
1408        });
1409
1410        // search_suggested should restore default options
1411        search_bar.update(cx, |search_bar, cx| {
1412            search_bar.search_suggested(cx);
1413            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1414        });
1415
1416        // toggling a search option should update the defaults
1417        search_bar
1418            .update(cx, |search_bar, cx| {
1419                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1420            })
1421            .await
1422            .unwrap();
1423        search_bar.update(cx, |search_bar, cx| {
1424            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1425        });
1426        let mut editor_notifications = cx.notifications(&editor);
1427        editor_notifications.next().await;
1428        editor.update(cx, |editor, cx| {
1429            assert_eq!(
1430                display_points_of(editor.all_text_background_highlights(cx)),
1431                &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1432            );
1433        });
1434
1435        // defaults should still include whole word
1436        search_bar.update(cx, |search_bar, cx| {
1437            search_bar.search_suggested(cx);
1438            assert_eq!(
1439                search_bar.search_options,
1440                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1441            )
1442        });
1443    }
1444
1445    #[gpui::test]
1446    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1447        init_globals(cx);
1448        let buffer_text = r#"
1449        A regular expression (shortened as regex or regexp;[1] also referred to as
1450        rational expression[2][3]) is a sequence of characters that specifies a search
1451        pattern in text. Usually such patterns are used by string-searching algorithms
1452        for "find" or "find and replace" operations on strings, or for input validation.
1453        "#
1454        .unindent();
1455        let expected_query_matches_count = buffer_text
1456            .chars()
1457            .filter(|c| c.to_ascii_lowercase() == 'a')
1458            .count();
1459        assert!(
1460            expected_query_matches_count > 1,
1461            "Should pick a query with multiple results"
1462        );
1463        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1464        let window = cx.add_window(|_| EmptyView {});
1465
1466        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1467
1468        let search_bar = window.build_view(cx, |cx| {
1469            let mut search_bar = BufferSearchBar::new(cx);
1470            search_bar.set_active_pane_item(Some(&editor), cx);
1471            search_bar.show(cx);
1472            search_bar
1473        });
1474
1475        window
1476            .update(cx, |_, cx| {
1477                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1478            })
1479            .unwrap()
1480            .await
1481            .unwrap();
1482        let initial_selections = window
1483            .update(cx, |_, cx| {
1484                search_bar.update(cx, |search_bar, cx| {
1485                    let handle = search_bar.query_editor.focus_handle(cx);
1486                    cx.focus(&handle);
1487                    search_bar.activate_current_match(cx);
1488                });
1489                assert!(
1490                    !editor.read(cx).is_focused(cx),
1491                    "Initially, the editor should not be focused"
1492                );
1493                let initial_selections = editor.update(cx, |editor, cx| {
1494                    let initial_selections = editor.selections.display_ranges(cx);
1495                    assert_eq!(
1496                        initial_selections.len(), 1,
1497                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1498                    );
1499                    initial_selections
1500                });
1501                search_bar.update(cx, |search_bar, cx| {
1502                    assert_eq!(search_bar.active_match_index, Some(0));
1503                    let handle = search_bar.query_editor.focus_handle(cx);
1504                    cx.focus(&handle);
1505                    search_bar.select_all_matches(&SelectAllMatches, cx);
1506                });
1507                assert!(
1508                    editor.read(cx).is_focused(cx),
1509                    "Should focus editor after successful SelectAllMatches"
1510                );
1511                search_bar.update(cx, |search_bar, cx| {
1512                    let all_selections =
1513                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1514                    assert_eq!(
1515                        all_selections.len(),
1516                        expected_query_matches_count,
1517                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1518                    );
1519                    assert_eq!(
1520                        search_bar.active_match_index,
1521                        Some(0),
1522                        "Match index should not change after selecting all matches"
1523                    );
1524                });
1525
1526                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1527                initial_selections
1528            }).unwrap();
1529
1530        window
1531            .update(cx, |_, cx| {
1532                assert!(
1533                    editor.read(cx).is_focused(cx),
1534                    "Should still have editor focused after SelectNextMatch"
1535                );
1536                search_bar.update(cx, |search_bar, cx| {
1537                    let all_selections =
1538                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1539                    assert_eq!(
1540                        all_selections.len(),
1541                        1,
1542                        "On next match, should deselect items and select the next match"
1543                    );
1544                    assert_ne!(
1545                        all_selections, initial_selections,
1546                        "Next match should be different from the first selection"
1547                    );
1548                    assert_eq!(
1549                        search_bar.active_match_index,
1550                        Some(1),
1551                        "Match index should be updated to the next one"
1552                    );
1553                    let handle = search_bar.query_editor.focus_handle(cx);
1554                    cx.focus(&handle);
1555                    search_bar.select_all_matches(&SelectAllMatches, cx);
1556                });
1557            })
1558            .unwrap();
1559        window
1560            .update(cx, |_, cx| {
1561                assert!(
1562                    editor.read(cx).is_focused(cx),
1563                    "Should focus editor after successful SelectAllMatches"
1564                );
1565                search_bar.update(cx, |search_bar, cx| {
1566                    let all_selections =
1567                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1568                    assert_eq!(
1569                    all_selections.len(),
1570                    expected_query_matches_count,
1571                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1572                );
1573                    assert_eq!(
1574                        search_bar.active_match_index,
1575                        Some(1),
1576                        "Match index should not change after selecting all matches"
1577                    );
1578                });
1579                search_bar.update(cx, |search_bar, cx| {
1580                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1581                });
1582            })
1583            .unwrap();
1584        let last_match_selections = window
1585            .update(cx, |_, cx| {
1586                assert!(
1587                    editor.read(cx).is_focused(&cx),
1588                    "Should still have editor focused after SelectPrevMatch"
1589                );
1590
1591                search_bar.update(cx, |search_bar, cx| {
1592                    let all_selections =
1593                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1594                    assert_eq!(
1595                        all_selections.len(),
1596                        1,
1597                        "On previous match, should deselect items and select the previous item"
1598                    );
1599                    assert_eq!(
1600                        all_selections, initial_selections,
1601                        "Previous match should be the same as the first selection"
1602                    );
1603                    assert_eq!(
1604                        search_bar.active_match_index,
1605                        Some(0),
1606                        "Match index should be updated to the previous one"
1607                    );
1608                    all_selections
1609                })
1610            })
1611            .unwrap();
1612
1613        window
1614            .update(cx, |_, cx| {
1615                search_bar.update(cx, |search_bar, cx| {
1616                    let handle = search_bar.query_editor.focus_handle(cx);
1617                    cx.focus(&handle);
1618                    search_bar.search("abas_nonexistent_match", None, cx)
1619                })
1620            })
1621            .unwrap()
1622            .await
1623            .unwrap();
1624        window
1625            .update(cx, |_, cx| {
1626                search_bar.update(cx, |search_bar, cx| {
1627                    search_bar.select_all_matches(&SelectAllMatches, cx);
1628                });
1629                assert!(
1630                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1631                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1632                );
1633                search_bar.update(cx, |search_bar, cx| {
1634                    let all_selections =
1635                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1636                    assert_eq!(
1637                        all_selections, last_match_selections,
1638                        "Should not select anything new if there are no matches"
1639                    );
1640                    assert!(
1641                        search_bar.active_match_index.is_none(),
1642                        "For no matches, there should be no active match index"
1643                    );
1644                });
1645            })
1646            .unwrap();
1647    }
1648
1649    #[gpui::test]
1650    async fn test_search_query_history(cx: &mut TestAppContext) {
1651        //crate::project_search::tests::init_test(cx);
1652        init_globals(cx);
1653        let buffer_text = r#"
1654        A regular expression (shortened as regex or regexp;[1] also referred to as
1655        rational expression[2][3]) is a sequence of characters that specifies a search
1656        pattern in text. Usually such patterns are used by string-searching algorithms
1657        for "find" or "find and replace" operations on strings, or for input validation.
1658        "#
1659        .unindent();
1660        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1661        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1662
1663        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1664
1665        let search_bar = cx.new_view(|cx| {
1666            let mut search_bar = BufferSearchBar::new(cx);
1667            search_bar.set_active_pane_item(Some(&editor), cx);
1668            search_bar.show(cx);
1669            search_bar
1670        });
1671
1672        // Add 3 search items into the history.
1673        search_bar
1674            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1675            .await
1676            .unwrap();
1677        search_bar
1678            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1679            .await
1680            .unwrap();
1681        search_bar
1682            .update(cx, |search_bar, cx| {
1683                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1684            })
1685            .await
1686            .unwrap();
1687        // Ensure that the latest search is active.
1688        search_bar.update(cx, |search_bar, cx| {
1689            assert_eq!(search_bar.query(cx), "c");
1690            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1691        });
1692
1693        // Next history query after the latest should set the query to the empty string.
1694        search_bar.update(cx, |search_bar, cx| {
1695            search_bar.next_history_query(&NextHistoryQuery, cx);
1696        });
1697        search_bar.update(cx, |search_bar, cx| {
1698            assert_eq!(search_bar.query(cx), "");
1699            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1700        });
1701        search_bar.update(cx, |search_bar, cx| {
1702            search_bar.next_history_query(&NextHistoryQuery, cx);
1703        });
1704        search_bar.update(cx, |search_bar, cx| {
1705            assert_eq!(search_bar.query(cx), "");
1706            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1707        });
1708
1709        // First previous query for empty current query should set the query to the latest.
1710        search_bar.update(cx, |search_bar, cx| {
1711            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1712        });
1713        search_bar.update(cx, |search_bar, cx| {
1714            assert_eq!(search_bar.query(cx), "c");
1715            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1716        });
1717
1718        // Further previous items should go over the history in reverse order.
1719        search_bar.update(cx, |search_bar, cx| {
1720            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1721        });
1722        search_bar.update(cx, |search_bar, cx| {
1723            assert_eq!(search_bar.query(cx), "b");
1724            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1725        });
1726
1727        // Previous items should never go behind the first history item.
1728        search_bar.update(cx, |search_bar, cx| {
1729            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1730        });
1731        search_bar.update(cx, |search_bar, cx| {
1732            assert_eq!(search_bar.query(cx), "a");
1733            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1734        });
1735        search_bar.update(cx, |search_bar, cx| {
1736            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1737        });
1738        search_bar.update(cx, |search_bar, cx| {
1739            assert_eq!(search_bar.query(cx), "a");
1740            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1741        });
1742
1743        // Next items should go over the history in the original order.
1744        search_bar.update(cx, |search_bar, cx| {
1745            search_bar.next_history_query(&NextHistoryQuery, cx);
1746        });
1747        search_bar.update(cx, |search_bar, cx| {
1748            assert_eq!(search_bar.query(cx), "b");
1749            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1750        });
1751
1752        search_bar
1753            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1754            .await
1755            .unwrap();
1756        search_bar.update(cx, |search_bar, cx| {
1757            assert_eq!(search_bar.query(cx), "ba");
1758            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1759        });
1760
1761        // New search input should add another entry to history and move the selection to the end of the history.
1762        search_bar.update(cx, |search_bar, cx| {
1763            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1764        });
1765        search_bar.update(cx, |search_bar, cx| {
1766            assert_eq!(search_bar.query(cx), "c");
1767            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1768        });
1769        search_bar.update(cx, |search_bar, cx| {
1770            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1771        });
1772        search_bar.update(cx, |search_bar, cx| {
1773            assert_eq!(search_bar.query(cx), "b");
1774            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1775        });
1776        search_bar.update(cx, |search_bar, cx| {
1777            search_bar.next_history_query(&NextHistoryQuery, cx);
1778        });
1779        search_bar.update(cx, |search_bar, cx| {
1780            assert_eq!(search_bar.query(cx), "c");
1781            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1782        });
1783        search_bar.update(cx, |search_bar, cx| {
1784            search_bar.next_history_query(&NextHistoryQuery, cx);
1785        });
1786        search_bar.update(cx, |search_bar, cx| {
1787            assert_eq!(search_bar.query(cx), "ba");
1788            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1789        });
1790        search_bar.update(cx, |search_bar, cx| {
1791            search_bar.next_history_query(&NextHistoryQuery, cx);
1792        });
1793        search_bar.update(cx, |search_bar, cx| {
1794            assert_eq!(search_bar.query(cx), "");
1795            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1796        });
1797    }
1798
1799    #[gpui::test]
1800    async fn test_replace_simple(cx: &mut TestAppContext) {
1801        let (editor, search_bar, cx) = init_test(cx);
1802
1803        search_bar
1804            .update(cx, |search_bar, cx| {
1805                search_bar.search("expression", None, cx)
1806            })
1807            .await
1808            .unwrap();
1809
1810        search_bar.update(cx, |search_bar, cx| {
1811            search_bar.replacement_editor.update(cx, |editor, cx| {
1812                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1813                editor.set_text("expr$1", cx);
1814            });
1815            search_bar.replace_all(&ReplaceAll, cx)
1816        });
1817        assert_eq!(
1818            editor.update(cx, |this, cx| { this.text(cx) }),
1819            r#"
1820        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1821        rational expr$1[2][3]) is a sequence of characters that specifies a search
1822        pattern in text. Usually such patterns are used by string-searching algorithms
1823        for "find" or "find and replace" operations on strings, or for input validation.
1824        "#
1825            .unindent()
1826        );
1827
1828        // Search for word boundaries and replace just a single one.
1829        search_bar
1830            .update(cx, |search_bar, cx| {
1831                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1832            })
1833            .await
1834            .unwrap();
1835
1836        search_bar.update(cx, |search_bar, cx| {
1837            search_bar.replacement_editor.update(cx, |editor, cx| {
1838                editor.set_text("banana", cx);
1839            });
1840            search_bar.replace_next(&ReplaceNext, cx)
1841        });
1842        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1843        assert_eq!(
1844            editor.update(cx, |this, cx| { this.text(cx) }),
1845            r#"
1846        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1847        rational expr$1[2][3]) is a sequence of characters that specifies a search
1848        pattern in text. Usually such patterns are used by string-searching algorithms
1849        for "find" or "find and replace" operations on strings, or for input validation.
1850        "#
1851            .unindent()
1852        );
1853        // Let's turn on regex mode.
1854        search_bar
1855            .update(cx, |search_bar, cx| {
1856                search_bar.activate_search_mode(SearchMode::Regex, cx);
1857                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1858            })
1859            .await
1860            .unwrap();
1861        search_bar.update(cx, |search_bar, cx| {
1862            search_bar.replacement_editor.update(cx, |editor, cx| {
1863                editor.set_text("${1}number", cx);
1864            });
1865            search_bar.replace_all(&ReplaceAll, cx)
1866        });
1867        assert_eq!(
1868            editor.update(cx, |this, cx| { this.text(cx) }),
1869            r#"
1870        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1871        rational expr$12number3number) is a sequence of characters that specifies a search
1872        pattern in text. Usually such patterns are used by string-searching algorithms
1873        for "find" or "find and replace" operations on strings, or for input validation.
1874        "#
1875            .unindent()
1876        );
1877        // Now with a whole-word twist.
1878        search_bar
1879            .update(cx, |search_bar, cx| {
1880                search_bar.activate_search_mode(SearchMode::Regex, cx);
1881                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1882            })
1883            .await
1884            .unwrap();
1885        search_bar.update(cx, |search_bar, cx| {
1886            search_bar.replacement_editor.update(cx, |editor, cx| {
1887                editor.set_text("things", cx);
1888            });
1889            search_bar.replace_all(&ReplaceAll, cx)
1890        });
1891        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1892        // of words in this text that would match this regex if not for WHOLE_WORD.
1893        assert_eq!(
1894            editor.update(cx, |this, cx| { this.text(cx) }),
1895            r#"
1896        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1897        rational expr$12number3number) is a sequence of characters that specifies a search
1898        pattern in text. Usually such patterns are used by string-searching things
1899        for "find" or "find and replace" operations on strings, or for input validation.
1900        "#
1901            .unindent()
1902        );
1903    }
1904}