buffer_search.rs

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