buffer_search.rs

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