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