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, 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(|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(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_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                                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_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::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_stack()
 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 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
1095    fn init_test(
1096        cx: &mut TestAppContext,
1097    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
1098        init_globals(cx);
1099        let buffer = cx.new_model(|cx| {
1100            Buffer::new(
1101                0,
1102                cx.entity_id().as_u64(),
1103                r#"
1104                A regular expression (shortened as regex or regexp;[1] also referred to as
1105                rational expression[2][3]) is a sequence of characters that specifies a search
1106                pattern in text. Usually such patterns are used by string-searching algorithms
1107                for "find" or "find and replace" operations on strings, or for input validation.
1108                "#
1109                .unindent(),
1110            )
1111        });
1112        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1113        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1114
1115        let search_bar = cx.new_view(|cx| {
1116            let mut search_bar = BufferSearchBar::new(cx);
1117            search_bar.set_active_pane_item(Some(&editor), cx);
1118            search_bar.show(cx);
1119            search_bar
1120        });
1121
1122        (editor, search_bar, cx)
1123    }
1124
1125    #[gpui::test]
1126    async fn test_search_simple(cx: &mut TestAppContext) {
1127        let (editor, search_bar, cx) = init_test(cx);
1128        // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
1129        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1130            background_highlights
1131                .into_iter()
1132                .map(|(range, _)| range)
1133                .collect::<Vec<_>>()
1134        };
1135        // Search for a string that appears with different casing.
1136        // By default, search is case-insensitive.
1137        search_bar
1138            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1139            .await
1140            .unwrap();
1141        editor.update(cx, |editor, cx| {
1142            assert_eq!(
1143                display_points_of(editor.all_text_background_highlights(cx)),
1144                &[
1145                    DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1146                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1147                ]
1148            );
1149        });
1150
1151        // Switch to a case sensitive search.
1152        search_bar.update(cx, |search_bar, cx| {
1153            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1154        });
1155        let mut editor_notifications = cx.notifications(&editor);
1156        editor_notifications.next().await;
1157        editor.update(cx, |editor, cx| {
1158            assert_eq!(
1159                display_points_of(editor.all_text_background_highlights(cx)),
1160                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1161            );
1162        });
1163
1164        // Search for a string that appears both as a whole word and
1165        // within other words. By default, all results are found.
1166        search_bar
1167            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1168            .await
1169            .unwrap();
1170        editor.update(cx, |editor, cx| {
1171            assert_eq!(
1172                display_points_of(editor.all_text_background_highlights(cx)),
1173                &[
1174                    DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1175                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1176                    DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1177                    DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1178                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1179                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1180                    DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1181                ]
1182            );
1183        });
1184
1185        // Switch to a whole word search.
1186        search_bar.update(cx, |search_bar, cx| {
1187            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1188        });
1189        let mut editor_notifications = cx.notifications(&editor);
1190        editor_notifications.next().await;
1191        editor.update(cx, |editor, cx| {
1192            assert_eq!(
1193                display_points_of(editor.all_text_background_highlights(cx)),
1194                &[
1195                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1196                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1197                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1198                ]
1199            );
1200        });
1201
1202        editor.update(cx, |editor, cx| {
1203            editor.change_selections(None, cx, |s| {
1204                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1205            });
1206        });
1207        search_bar.update(cx, |search_bar, cx| {
1208            assert_eq!(search_bar.active_match_index, Some(0));
1209            search_bar.select_next_match(&SelectNextMatch, cx);
1210            assert_eq!(
1211                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1212                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1213            );
1214        });
1215        search_bar.update(cx, |search_bar, _| {
1216            assert_eq!(search_bar.active_match_index, Some(0));
1217        });
1218
1219        search_bar.update(cx, |search_bar, cx| {
1220            search_bar.select_next_match(&SelectNextMatch, cx);
1221            assert_eq!(
1222                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1223                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1224            );
1225        });
1226        search_bar.update(cx, |search_bar, _| {
1227            assert_eq!(search_bar.active_match_index, Some(1));
1228        });
1229
1230        search_bar.update(cx, |search_bar, cx| {
1231            search_bar.select_next_match(&SelectNextMatch, cx);
1232            assert_eq!(
1233                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1234                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1235            );
1236        });
1237        search_bar.update(cx, |search_bar, _| {
1238            assert_eq!(search_bar.active_match_index, Some(2));
1239        });
1240
1241        search_bar.update(cx, |search_bar, cx| {
1242            search_bar.select_next_match(&SelectNextMatch, cx);
1243            assert_eq!(
1244                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1245                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1246            );
1247        });
1248        search_bar.update(cx, |search_bar, _| {
1249            assert_eq!(search_bar.active_match_index, Some(0));
1250        });
1251
1252        search_bar.update(cx, |search_bar, cx| {
1253            search_bar.select_prev_match(&SelectPrevMatch, cx);
1254            assert_eq!(
1255                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1256                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1257            );
1258        });
1259        search_bar.update(cx, |search_bar, _| {
1260            assert_eq!(search_bar.active_match_index, Some(2));
1261        });
1262
1263        search_bar.update(cx, |search_bar, cx| {
1264            search_bar.select_prev_match(&SelectPrevMatch, cx);
1265            assert_eq!(
1266                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1267                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1268            );
1269        });
1270        search_bar.update(cx, |search_bar, _| {
1271            assert_eq!(search_bar.active_match_index, Some(1));
1272        });
1273
1274        search_bar.update(cx, |search_bar, cx| {
1275            search_bar.select_prev_match(&SelectPrevMatch, cx);
1276            assert_eq!(
1277                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1278                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1279            );
1280        });
1281        search_bar.update(cx, |search_bar, _| {
1282            assert_eq!(search_bar.active_match_index, Some(0));
1283        });
1284
1285        // Park the cursor in between matches and ensure that going to the previous match selects
1286        // the closest match to the left.
1287        editor.update(cx, |editor, cx| {
1288            editor.change_selections(None, cx, |s| {
1289                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1290            });
1291        });
1292        search_bar.update(cx, |search_bar, cx| {
1293            assert_eq!(search_bar.active_match_index, Some(1));
1294            search_bar.select_prev_match(&SelectPrevMatch, cx);
1295            assert_eq!(
1296                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1297                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1298            );
1299        });
1300        search_bar.update(cx, |search_bar, _| {
1301            assert_eq!(search_bar.active_match_index, Some(0));
1302        });
1303
1304        // Park the cursor in between matches and ensure that going to the next match selects the
1305        // closest match to the right.
1306        editor.update(cx, |editor, cx| {
1307            editor.change_selections(None, cx, |s| {
1308                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1309            });
1310        });
1311        search_bar.update(cx, |search_bar, cx| {
1312            assert_eq!(search_bar.active_match_index, Some(1));
1313            search_bar.select_next_match(&SelectNextMatch, cx);
1314            assert_eq!(
1315                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1316                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1317            );
1318        });
1319        search_bar.update(cx, |search_bar, _| {
1320            assert_eq!(search_bar.active_match_index, Some(1));
1321        });
1322
1323        // Park the cursor after the last match and ensure that going to the previous match selects
1324        // the last match.
1325        editor.update(cx, |editor, cx| {
1326            editor.change_selections(None, cx, |s| {
1327                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1328            });
1329        });
1330        search_bar.update(cx, |search_bar, cx| {
1331            assert_eq!(search_bar.active_match_index, Some(2));
1332            search_bar.select_prev_match(&SelectPrevMatch, cx);
1333            assert_eq!(
1334                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1335                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1336            );
1337        });
1338        search_bar.update(cx, |search_bar, _| {
1339            assert_eq!(search_bar.active_match_index, Some(2));
1340        });
1341
1342        // Park the cursor after the last match and ensure that going to the next match selects the
1343        // first match.
1344        editor.update(cx, |editor, cx| {
1345            editor.change_selections(None, cx, |s| {
1346                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1347            });
1348        });
1349        search_bar.update(cx, |search_bar, cx| {
1350            assert_eq!(search_bar.active_match_index, Some(2));
1351            search_bar.select_next_match(&SelectNextMatch, cx);
1352            assert_eq!(
1353                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1354                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1355            );
1356        });
1357        search_bar.update(cx, |search_bar, _| {
1358            assert_eq!(search_bar.active_match_index, Some(0));
1359        });
1360
1361        // Park the cursor before the first match and ensure that going to the previous match
1362        // selects the last match.
1363        editor.update(cx, |editor, cx| {
1364            editor.change_selections(None, cx, |s| {
1365                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1366            });
1367        });
1368        search_bar.update(cx, |search_bar, cx| {
1369            assert_eq!(search_bar.active_match_index, Some(0));
1370            search_bar.select_prev_match(&SelectPrevMatch, cx);
1371            assert_eq!(
1372                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1373                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1374            );
1375        });
1376        search_bar.update(cx, |search_bar, _| {
1377            assert_eq!(search_bar.active_match_index, Some(2));
1378        });
1379    }
1380
1381    #[gpui::test]
1382    async fn test_search_option_handling(cx: &mut TestAppContext) {
1383        let (editor, search_bar, cx) = init_test(cx);
1384
1385        // show with options should make current search case sensitive
1386        search_bar
1387            .update(cx, |search_bar, cx| {
1388                search_bar.show(cx);
1389                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1390            })
1391            .await
1392            .unwrap();
1393        // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1394        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1395            background_highlights
1396                .into_iter()
1397                .map(|(range, _)| range)
1398                .collect::<Vec<_>>()
1399        };
1400        editor.update(cx, |editor, cx| {
1401            assert_eq!(
1402                display_points_of(editor.all_text_background_highlights(cx)),
1403                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1404            );
1405        });
1406
1407        // search_suggested should restore default options
1408        search_bar.update(cx, |search_bar, cx| {
1409            search_bar.search_suggested(cx);
1410            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1411        });
1412
1413        // toggling a search option should update the defaults
1414        search_bar
1415            .update(cx, |search_bar, cx| {
1416                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1417            })
1418            .await
1419            .unwrap();
1420        search_bar.update(cx, |search_bar, cx| {
1421            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1422        });
1423        let mut editor_notifications = cx.notifications(&editor);
1424        editor_notifications.next().await;
1425        editor.update(cx, |editor, cx| {
1426            assert_eq!(
1427                display_points_of(editor.all_text_background_highlights(cx)),
1428                &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1429            );
1430        });
1431
1432        // defaults should still include whole word
1433        search_bar.update(cx, |search_bar, cx| {
1434            search_bar.search_suggested(cx);
1435            assert_eq!(
1436                search_bar.search_options,
1437                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1438            )
1439        });
1440    }
1441
1442    #[gpui::test]
1443    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1444        init_globals(cx);
1445        let buffer_text = r#"
1446        A regular expression (shortened as regex or regexp;[1] also referred to as
1447        rational expression[2][3]) is a sequence of characters that specifies a search
1448        pattern in text. Usually such patterns are used by string-searching algorithms
1449        for "find" or "find and replace" operations on strings, or for input validation.
1450        "#
1451        .unindent();
1452        let expected_query_matches_count = buffer_text
1453            .chars()
1454            .filter(|c| c.to_ascii_lowercase() == 'a')
1455            .count();
1456        assert!(
1457            expected_query_matches_count > 1,
1458            "Should pick a query with multiple results"
1459        );
1460        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1461        let window = cx.add_window(|_| EmptyView {});
1462
1463        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1464
1465        let search_bar = window.build_view(cx, |cx| {
1466            let mut search_bar = BufferSearchBar::new(cx);
1467            search_bar.set_active_pane_item(Some(&editor), cx);
1468            search_bar.show(cx);
1469            search_bar
1470        });
1471
1472        window
1473            .update(cx, |_, cx| {
1474                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1475            })
1476            .unwrap()
1477            .await
1478            .unwrap();
1479        let initial_selections = window
1480            .update(cx, |_, cx| {
1481                search_bar.update(cx, |search_bar, cx| {
1482                    let handle = search_bar.query_editor.focus_handle(cx);
1483                    cx.focus(&handle);
1484                    search_bar.activate_current_match(cx);
1485                });
1486                assert!(
1487                    !editor.read(cx).is_focused(cx),
1488                    "Initially, the editor should not be focused"
1489                );
1490                let initial_selections = editor.update(cx, |editor, cx| {
1491                    let initial_selections = editor.selections.display_ranges(cx);
1492                    assert_eq!(
1493                        initial_selections.len(), 1,
1494                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1495                    );
1496                    initial_selections
1497                });
1498                search_bar.update(cx, |search_bar, cx| {
1499                    assert_eq!(search_bar.active_match_index, Some(0));
1500                    let handle = search_bar.query_editor.focus_handle(cx);
1501                    cx.focus(&handle);
1502                    search_bar.select_all_matches(&SelectAllMatches, cx);
1503                });
1504                assert!(
1505                    editor.read(cx).is_focused(cx),
1506                    "Should focus editor after successful SelectAllMatches"
1507                );
1508                search_bar.update(cx, |search_bar, cx| {
1509                    let all_selections =
1510                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1511                    assert_eq!(
1512                        all_selections.len(),
1513                        expected_query_matches_count,
1514                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1515                    );
1516                    assert_eq!(
1517                        search_bar.active_match_index,
1518                        Some(0),
1519                        "Match index should not change after selecting all matches"
1520                    );
1521                });
1522
1523                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1524                initial_selections
1525            }).unwrap();
1526
1527        window
1528            .update(cx, |_, cx| {
1529                assert!(
1530                    editor.read(cx).is_focused(cx),
1531                    "Should still have editor focused after SelectNextMatch"
1532                );
1533                search_bar.update(cx, |search_bar, cx| {
1534                    let all_selections =
1535                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1536                    assert_eq!(
1537                        all_selections.len(),
1538                        1,
1539                        "On next match, should deselect items and select the next match"
1540                    );
1541                    assert_ne!(
1542                        all_selections, initial_selections,
1543                        "Next match should be different from the first selection"
1544                    );
1545                    assert_eq!(
1546                        search_bar.active_match_index,
1547                        Some(1),
1548                        "Match index should be updated to the next one"
1549                    );
1550                    let handle = search_bar.query_editor.focus_handle(cx);
1551                    cx.focus(&handle);
1552                    search_bar.select_all_matches(&SelectAllMatches, cx);
1553                });
1554            })
1555            .unwrap();
1556        window
1557            .update(cx, |_, cx| {
1558                assert!(
1559                    editor.read(cx).is_focused(cx),
1560                    "Should focus editor after successful SelectAllMatches"
1561                );
1562                search_bar.update(cx, |search_bar, cx| {
1563                    let all_selections =
1564                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1565                    assert_eq!(
1566                    all_selections.len(),
1567                    expected_query_matches_count,
1568                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1569                );
1570                    assert_eq!(
1571                        search_bar.active_match_index,
1572                        Some(1),
1573                        "Match index should not change after selecting all matches"
1574                    );
1575                });
1576                search_bar.update(cx, |search_bar, cx| {
1577                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1578                });
1579            })
1580            .unwrap();
1581        let last_match_selections = window
1582            .update(cx, |_, cx| {
1583                assert!(
1584                    editor.read(cx).is_focused(&cx),
1585                    "Should still have editor focused after SelectPrevMatch"
1586                );
1587
1588                search_bar.update(cx, |search_bar, cx| {
1589                    let all_selections =
1590                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1591                    assert_eq!(
1592                        all_selections.len(),
1593                        1,
1594                        "On previous match, should deselect items and select the previous item"
1595                    );
1596                    assert_eq!(
1597                        all_selections, initial_selections,
1598                        "Previous match should be the same as the first selection"
1599                    );
1600                    assert_eq!(
1601                        search_bar.active_match_index,
1602                        Some(0),
1603                        "Match index should be updated to the previous one"
1604                    );
1605                    all_selections
1606                })
1607            })
1608            .unwrap();
1609
1610        window
1611            .update(cx, |_, cx| {
1612                search_bar.update(cx, |search_bar, cx| {
1613                    let handle = search_bar.query_editor.focus_handle(cx);
1614                    cx.focus(&handle);
1615                    search_bar.search("abas_nonexistent_match", None, cx)
1616                })
1617            })
1618            .unwrap()
1619            .await
1620            .unwrap();
1621        window
1622            .update(cx, |_, cx| {
1623                search_bar.update(cx, |search_bar, cx| {
1624                    search_bar.select_all_matches(&SelectAllMatches, cx);
1625                });
1626                assert!(
1627                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1628                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1629                );
1630                search_bar.update(cx, |search_bar, cx| {
1631                    let all_selections =
1632                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1633                    assert_eq!(
1634                        all_selections, last_match_selections,
1635                        "Should not select anything new if there are no matches"
1636                    );
1637                    assert!(
1638                        search_bar.active_match_index.is_none(),
1639                        "For no matches, there should be no active match index"
1640                    );
1641                });
1642            })
1643            .unwrap();
1644    }
1645
1646    #[gpui::test]
1647    async fn test_search_query_history(cx: &mut TestAppContext) {
1648        //crate::project_search::tests::init_test(cx);
1649        init_globals(cx);
1650        let buffer_text = r#"
1651        A regular expression (shortened as regex or regexp;[1] also referred to as
1652        rational expression[2][3]) is a sequence of characters that specifies a search
1653        pattern in text. Usually such patterns are used by string-searching algorithms
1654        for "find" or "find and replace" operations on strings, or for input validation.
1655        "#
1656        .unindent();
1657        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1658        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1659
1660        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1661
1662        let search_bar = cx.new_view(|cx| {
1663            let mut search_bar = BufferSearchBar::new(cx);
1664            search_bar.set_active_pane_item(Some(&editor), cx);
1665            search_bar.show(cx);
1666            search_bar
1667        });
1668
1669        // Add 3 search items into the history.
1670        search_bar
1671            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1672            .await
1673            .unwrap();
1674        search_bar
1675            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1676            .await
1677            .unwrap();
1678        search_bar
1679            .update(cx, |search_bar, cx| {
1680                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1681            })
1682            .await
1683            .unwrap();
1684        // Ensure that the latest search is active.
1685        search_bar.update(cx, |search_bar, cx| {
1686            assert_eq!(search_bar.query(cx), "c");
1687            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1688        });
1689
1690        // Next history query after the latest should set the query to the empty string.
1691        search_bar.update(cx, |search_bar, cx| {
1692            search_bar.next_history_query(&NextHistoryQuery, cx);
1693        });
1694        search_bar.update(cx, |search_bar, cx| {
1695            assert_eq!(search_bar.query(cx), "");
1696            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1697        });
1698        search_bar.update(cx, |search_bar, cx| {
1699            search_bar.next_history_query(&NextHistoryQuery, cx);
1700        });
1701        search_bar.update(cx, |search_bar, cx| {
1702            assert_eq!(search_bar.query(cx), "");
1703            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1704        });
1705
1706        // First previous query for empty current query should set the query to the latest.
1707        search_bar.update(cx, |search_bar, cx| {
1708            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1709        });
1710        search_bar.update(cx, |search_bar, cx| {
1711            assert_eq!(search_bar.query(cx), "c");
1712            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1713        });
1714
1715        // Further previous items should go over the history in reverse order.
1716        search_bar.update(cx, |search_bar, cx| {
1717            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1718        });
1719        search_bar.update(cx, |search_bar, cx| {
1720            assert_eq!(search_bar.query(cx), "b");
1721            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1722        });
1723
1724        // Previous items should never go behind the first history item.
1725        search_bar.update(cx, |search_bar, cx| {
1726            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1727        });
1728        search_bar.update(cx, |search_bar, cx| {
1729            assert_eq!(search_bar.query(cx), "a");
1730            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1731        });
1732        search_bar.update(cx, |search_bar, cx| {
1733            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1734        });
1735        search_bar.update(cx, |search_bar, cx| {
1736            assert_eq!(search_bar.query(cx), "a");
1737            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1738        });
1739
1740        // Next items should go over the history in the original order.
1741        search_bar.update(cx, |search_bar, cx| {
1742            search_bar.next_history_query(&NextHistoryQuery, cx);
1743        });
1744        search_bar.update(cx, |search_bar, cx| {
1745            assert_eq!(search_bar.query(cx), "b");
1746            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1747        });
1748
1749        search_bar
1750            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1751            .await
1752            .unwrap();
1753        search_bar.update(cx, |search_bar, cx| {
1754            assert_eq!(search_bar.query(cx), "ba");
1755            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1756        });
1757
1758        // New search input should add another entry to history and move the selection to the end of the history.
1759        search_bar.update(cx, |search_bar, cx| {
1760            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1761        });
1762        search_bar.update(cx, |search_bar, cx| {
1763            assert_eq!(search_bar.query(cx), "c");
1764            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1765        });
1766        search_bar.update(cx, |search_bar, cx| {
1767            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1768        });
1769        search_bar.update(cx, |search_bar, cx| {
1770            assert_eq!(search_bar.query(cx), "b");
1771            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1772        });
1773        search_bar.update(cx, |search_bar, cx| {
1774            search_bar.next_history_query(&NextHistoryQuery, cx);
1775        });
1776        search_bar.update(cx, |search_bar, cx| {
1777            assert_eq!(search_bar.query(cx), "c");
1778            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1779        });
1780        search_bar.update(cx, |search_bar, cx| {
1781            search_bar.next_history_query(&NextHistoryQuery, cx);
1782        });
1783        search_bar.update(cx, |search_bar, cx| {
1784            assert_eq!(search_bar.query(cx), "ba");
1785            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1786        });
1787        search_bar.update(cx, |search_bar, cx| {
1788            search_bar.next_history_query(&NextHistoryQuery, cx);
1789        });
1790        search_bar.update(cx, |search_bar, cx| {
1791            assert_eq!(search_bar.query(cx), "");
1792            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1793        });
1794    }
1795
1796    #[gpui::test]
1797    async fn test_replace_simple(cx: &mut TestAppContext) {
1798        let (editor, search_bar, cx) = init_test(cx);
1799
1800        search_bar
1801            .update(cx, |search_bar, cx| {
1802                search_bar.search("expression", None, cx)
1803            })
1804            .await
1805            .unwrap();
1806
1807        search_bar.update(cx, |search_bar, cx| {
1808            search_bar.replacement_editor.update(cx, |editor, cx| {
1809                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1810                editor.set_text("expr$1", cx);
1811            });
1812            search_bar.replace_all(&ReplaceAll, cx)
1813        });
1814        assert_eq!(
1815            editor.update(cx, |this, cx| { this.text(cx) }),
1816            r#"
1817        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1818        rational expr$1[2][3]) is a sequence of characters that specifies a search
1819        pattern in text. Usually such patterns are used by string-searching algorithms
1820        for "find" or "find and replace" operations on strings, or for input validation.
1821        "#
1822            .unindent()
1823        );
1824
1825        // Search for word boundaries and replace just a single one.
1826        search_bar
1827            .update(cx, |search_bar, cx| {
1828                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1829            })
1830            .await
1831            .unwrap();
1832
1833        search_bar.update(cx, |search_bar, cx| {
1834            search_bar.replacement_editor.update(cx, |editor, cx| {
1835                editor.set_text("banana", cx);
1836            });
1837            search_bar.replace_next(&ReplaceNext, cx)
1838        });
1839        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1840        assert_eq!(
1841            editor.update(cx, |this, cx| { this.text(cx) }),
1842            r#"
1843        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1844        rational expr$1[2][3]) is a sequence of characters that specifies a search
1845        pattern in text. Usually such patterns are used by string-searching algorithms
1846        for "find" or "find and replace" operations on strings, or for input validation.
1847        "#
1848            .unindent()
1849        );
1850        // Let's turn on regex mode.
1851        search_bar
1852            .update(cx, |search_bar, cx| {
1853                search_bar.activate_search_mode(SearchMode::Regex, cx);
1854                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1855            })
1856            .await
1857            .unwrap();
1858        search_bar.update(cx, |search_bar, cx| {
1859            search_bar.replacement_editor.update(cx, |editor, cx| {
1860                editor.set_text("${1}number", cx);
1861            });
1862            search_bar.replace_all(&ReplaceAll, cx)
1863        });
1864        assert_eq!(
1865            editor.update(cx, |this, cx| { this.text(cx) }),
1866            r#"
1867        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1868        rational expr$12number3number) is a sequence of characters that specifies a search
1869        pattern in text. Usually such patterns are used by string-searching algorithms
1870        for "find" or "find and replace" operations on strings, or for input validation.
1871        "#
1872            .unindent()
1873        );
1874        // Now with a whole-word twist.
1875        search_bar
1876            .update(cx, |search_bar, cx| {
1877                search_bar.activate_search_mode(SearchMode::Regex, cx);
1878                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1879            })
1880            .await
1881            .unwrap();
1882        search_bar.update(cx, |search_bar, cx| {
1883            search_bar.replacement_editor.update(cx, |editor, cx| {
1884                editor.set_text("things", cx);
1885            });
1886            search_bar.replace_all(&ReplaceAll, cx)
1887        });
1888        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1889        // of words in this text that would match this regex if not for WHOLE_WORD.
1890        assert_eq!(
1891            editor.update(cx, |this, cx| { this.text(cx) }),
1892            r#"
1893        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1894        rational expr$12number3number) is a sequence of characters that specifies a search
1895        pattern in text. Usually such patterns are used by string-searching things
1896        for "find" or "find and replace" operations on strings, or for input validation.
1897        "#
1898            .unindent()
1899        );
1900    }
1901}