buffer_search.rs

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