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