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