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