buffer_search.rs

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