buffer_search.rs

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