buffer_search.rs

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