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
 425impl BufferSearchBar {
 426    fn register(workspace: &mut Workspace) {
 427        workspace.register_action(move |workspace, deploy: &Deploy, cx| {
 428            let pane = workspace.active_pane();
 429
 430            pane.update(cx, |this, cx| {
 431                this.toolbar().update(cx, |this, cx| {
 432                    if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
 433                        search_bar.update(cx, |this, cx| {
 434                            this.deploy(deploy, cx);
 435                        });
 436                        return;
 437                    }
 438                    let view = cx.new_view(|cx| BufferSearchBar::new(cx));
 439                    this.add_item(view.clone(), cx);
 440                    view.update(cx, |this, cx| this.deploy(deploy, cx));
 441                    cx.notify();
 442                })
 443            });
 444        });
 445        fn register_action<A: Action>(
 446            workspace: &mut Workspace,
 447            update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
 448        ) {
 449            workspace.register_action(move |workspace, action: &A, cx| {
 450                let pane = workspace.active_pane();
 451                pane.update(cx, move |this, cx| {
 452                    this.toolbar().update(cx, move |this, cx| {
 453                        if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
 454                            search_bar.update(cx, move |this, cx| update(this, action, cx));
 455                            cx.notify();
 456                        }
 457                    })
 458                });
 459            });
 460        }
 461
 462        register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
 463            if this.supported_options().case {
 464                this.toggle_case_sensitive(action, cx);
 465            }
 466        });
 467        register_action(workspace, |this, action: &ToggleWholeWord, cx| {
 468            if this.supported_options().word {
 469                this.toggle_whole_word(action, cx);
 470            }
 471        });
 472        register_action(workspace, |this, action: &ToggleReplace, cx| {
 473            if this.supported_options().replacement {
 474                this.toggle_replace(action, cx);
 475            }
 476        });
 477        register_action(workspace, |this, _: &ActivateRegexMode, cx| {
 478            if this.supported_options().regex {
 479                this.activate_search_mode(SearchMode::Regex, cx);
 480            }
 481        });
 482        register_action(workspace, |this, _: &ActivateTextMode, cx| {
 483            this.activate_search_mode(SearchMode::Text, cx);
 484        });
 485        register_action(workspace, |this, action: &CycleMode, cx| {
 486            if this.supported_options().regex {
 487                // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
 488                // cycling.
 489                this.cycle_mode(action, cx)
 490            }
 491        });
 492        register_action(workspace, |this, action: &SelectNextMatch, cx| {
 493            this.select_next_match(action, cx);
 494        });
 495        register_action(workspace, |this, action: &SelectPrevMatch, cx| {
 496            this.select_prev_match(action, cx);
 497        });
 498        register_action(workspace, |this, action: &SelectAllMatches, cx| {
 499            this.select_all_matches(action, cx);
 500        });
 501        register_action(workspace, |this, _: &editor::Cancel, cx| {
 502            if !this.dismissed {
 503                this.dismiss(&Dismiss, cx);
 504                return;
 505            }
 506            cx.propagate();
 507        });
 508    }
 509    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 510        let query_editor = cx.new_view(|cx| Editor::single_line(cx));
 511        cx.subscribe(&query_editor, Self::on_query_editor_event)
 512            .detach();
 513        let replacement_editor = cx.new_view(|cx| Editor::single_line(cx));
 514        cx.subscribe(&replacement_editor, Self::on_query_editor_event)
 515            .detach();
 516        Self {
 517            query_editor,
 518            replacement_editor,
 519            active_searchable_item: None,
 520            active_searchable_item_subscription: None,
 521            active_match_index: None,
 522            searchable_items_with_matches: Default::default(),
 523            default_options: SearchOptions::NONE,
 524            search_options: SearchOptions::NONE,
 525            pending_search: None,
 526            query_contains_error: false,
 527            dismissed: true,
 528            search_history: SearchHistory::default(),
 529            current_mode: SearchMode::default(),
 530            active_search: None,
 531            replace_enabled: false,
 532        }
 533    }
 534
 535    pub fn is_dismissed(&self) -> bool {
 536        self.dismissed
 537    }
 538
 539    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
 540        self.dismissed = true;
 541        for searchable_item in self.searchable_items_with_matches.keys() {
 542            if let Some(searchable_item) =
 543                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 544            {
 545                searchable_item.clear_matches(cx);
 546            }
 547        }
 548        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 549            let handle = active_editor.focus_handle(cx);
 550            cx.focus(&handle);
 551        }
 552        cx.emit(Event::UpdateLocation);
 553        cx.emit(ToolbarItemEvent::ChangeLocation(
 554            ToolbarItemLocation::Hidden,
 555        ));
 556        cx.notify();
 557    }
 558
 559    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
 560        if self.show(cx) {
 561            self.search_suggested(cx);
 562            if deploy.focus {
 563                self.select_query(cx);
 564                let handle = self.query_editor.focus_handle(cx);
 565                cx.focus(&handle);
 566            }
 567            return true;
 568        }
 569
 570        false
 571    }
 572
 573    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
 574        if self.is_dismissed() {
 575            self.deploy(action, cx);
 576        } else {
 577            self.dismiss(&Dismiss, cx);
 578        }
 579    }
 580
 581    pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
 582        if self.active_searchable_item.is_none() {
 583            return false;
 584        }
 585        self.dismissed = false;
 586        cx.notify();
 587        cx.emit(Event::UpdateLocation);
 588        cx.emit(ToolbarItemEvent::ChangeLocation(
 589            ToolbarItemLocation::Secondary,
 590        ));
 591        true
 592    }
 593
 594    fn supported_options(&self) -> workspace::searchable::SearchOptions {
 595        self.active_searchable_item
 596            .as_deref()
 597            .map(SearchableItemHandle::supported_options)
 598            .unwrap_or_default()
 599    }
 600    pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
 601        let search = self
 602            .query_suggestion(cx)
 603            .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
 604
 605        if let Some(search) = search {
 606            cx.spawn(|this, mut cx| async move {
 607                search.await?;
 608                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 609            })
 610            .detach_and_log_err(cx);
 611        }
 612    }
 613
 614    pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
 615        if let Some(match_ix) = self.active_match_index {
 616            if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 617                if let Some(matches) = self
 618                    .searchable_items_with_matches
 619                    .get(&active_searchable_item.downgrade())
 620                {
 621                    active_searchable_item.activate_match(match_ix, matches, cx)
 622                }
 623            }
 624        }
 625    }
 626
 627    pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
 628        self.query_editor.update(cx, |query_editor, cx| {
 629            query_editor.select_all(&Default::default(), cx);
 630        });
 631    }
 632
 633    pub fn query(&self, cx: &WindowContext) -> String {
 634        self.query_editor.read(cx).text(cx)
 635    }
 636    pub fn replacement(&self, cx: &WindowContext) -> String {
 637        self.replacement_editor.read(cx).text(cx)
 638    }
 639    pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
 640        self.active_searchable_item
 641            .as_ref()
 642            .map(|searchable_item| searchable_item.query_suggestion(cx))
 643            .filter(|suggestion| !suggestion.is_empty())
 644    }
 645
 646    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
 647        if replacement.is_none() {
 648            self.replace_enabled = false;
 649            return;
 650        }
 651        self.replace_enabled = true;
 652        self.replacement_editor
 653            .update(cx, |replacement_editor, cx| {
 654                replacement_editor
 655                    .buffer()
 656                    .update(cx, |replacement_buffer, cx| {
 657                        let len = replacement_buffer.len(cx);
 658                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
 659                    });
 660            });
 661    }
 662
 663    pub fn search(
 664        &mut self,
 665        query: &str,
 666        options: Option<SearchOptions>,
 667        cx: &mut ViewContext<Self>,
 668    ) -> oneshot::Receiver<()> {
 669        let options = options.unwrap_or(self.default_options);
 670        if query != self.query(cx) || self.search_options != options {
 671            self.query_editor.update(cx, |query_editor, cx| {
 672                query_editor.buffer().update(cx, |query_buffer, cx| {
 673                    let len = query_buffer.len(cx);
 674                    query_buffer.edit([(0..len, query)], None, cx);
 675                });
 676            });
 677            self.search_options = options;
 678            self.query_contains_error = false;
 679            self.clear_matches(cx);
 680            cx.notify();
 681        }
 682        self.update_matches(cx)
 683    }
 684
 685    fn render_search_option_button(
 686        &self,
 687        option: SearchOptions,
 688        action: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 689    ) -> impl IntoElement {
 690        let is_active = self.search_options.contains(option);
 691        option.as_button(is_active, action)
 692    }
 693    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 694        assert_ne!(
 695            mode,
 696            SearchMode::Semantic,
 697            "Semantic search is not supported in buffer search"
 698        );
 699        if mode == self.current_mode {
 700            return;
 701        }
 702        self.current_mode = mode;
 703        let _ = self.update_matches(cx);
 704        cx.notify();
 705    }
 706
 707    pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 708        if let Some(active_editor) = self.active_searchable_item.as_ref() {
 709            let handle = active_editor.focus_handle(cx);
 710            cx.focus(&handle);
 711        }
 712    }
 713
 714    fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
 715        self.search_options.toggle(search_option);
 716        self.default_options = self.search_options;
 717        let _ = self.update_matches(cx);
 718        cx.notify();
 719    }
 720
 721    pub fn set_search_options(
 722        &mut self,
 723        search_options: SearchOptions,
 724        cx: &mut ViewContext<Self>,
 725    ) {
 726        self.search_options = search_options;
 727        cx.notify();
 728    }
 729
 730    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
 731        self.select_match(Direction::Next, 1, cx);
 732    }
 733
 734    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
 735        self.select_match(Direction::Prev, 1, cx);
 736    }
 737
 738    fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
 739        if !self.dismissed && self.active_match_index.is_some() {
 740            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 741                if let Some(matches) = self
 742                    .searchable_items_with_matches
 743                    .get(&searchable_item.downgrade())
 744                {
 745                    searchable_item.select_matches(matches, cx);
 746                    self.focus_editor(&FocusEditor, cx);
 747                }
 748            }
 749        }
 750    }
 751
 752    pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
 753        if let Some(index) = self.active_match_index {
 754            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 755                if let Some(matches) = self
 756                    .searchable_items_with_matches
 757                    .get(&searchable_item.downgrade())
 758                {
 759                    let new_match_index = searchable_item
 760                        .match_index_for_direction(matches, index, direction, count, cx);
 761
 762                    searchable_item.update_matches(matches, cx);
 763                    searchable_item.activate_match(new_match_index, matches, cx);
 764                }
 765            }
 766        }
 767    }
 768
 769    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
 770        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 771            if let Some(matches) = self
 772                .searchable_items_with_matches
 773                .get(&searchable_item.downgrade())
 774            {
 775                if matches.len() == 0 {
 776                    return;
 777                }
 778                let new_match_index = matches.len() - 1;
 779                searchable_item.update_matches(matches, cx);
 780                searchable_item.activate_match(new_match_index, matches, cx);
 781            }
 782        }
 783    }
 784
 785    fn on_query_editor_event(
 786        &mut self,
 787        _: View<Editor>,
 788        event: &editor::EditorEvent,
 789        cx: &mut ViewContext<Self>,
 790    ) {
 791        if let editor::EditorEvent::Edited { .. } = event {
 792            self.query_contains_error = false;
 793            self.clear_matches(cx);
 794            let search = self.update_matches(cx);
 795            cx.spawn(|this, mut cx| async move {
 796                search.await?;
 797                this.update(&mut cx, |this, cx| this.activate_current_match(cx))
 798            })
 799            .detach_and_log_err(cx);
 800        }
 801    }
 802
 803    fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
 804        match event {
 805            SearchEvent::MatchesInvalidated => {
 806                let _ = self.update_matches(cx);
 807            }
 808            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
 809        }
 810    }
 811
 812    fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
 813        self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
 814    }
 815    fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
 816        self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
 817    }
 818    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 819        let mut active_item_matches = None;
 820        for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
 821            if let Some(searchable_item) =
 822                WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
 823            {
 824                if Some(&searchable_item) == self.active_searchable_item.as_ref() {
 825                    active_item_matches = Some((searchable_item.downgrade(), matches));
 826                } else {
 827                    searchable_item.clear_matches(cx);
 828                }
 829            }
 830        }
 831
 832        self.searchable_items_with_matches
 833            .extend(active_item_matches);
 834    }
 835
 836    fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
 837        let (done_tx, done_rx) = oneshot::channel();
 838        let query = self.query(cx);
 839        self.pending_search.take();
 840
 841        if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
 842            if query.is_empty() {
 843                self.active_match_index.take();
 844                active_searchable_item.clear_matches(cx);
 845                let _ = done_tx.send(());
 846                cx.notify();
 847            } else {
 848                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
 849                    match SearchQuery::regex(
 850                        query,
 851                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 852                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 853                        false,
 854                        Vec::new(),
 855                        Vec::new(),
 856                    ) {
 857                        Ok(query) => query.with_replacement(self.replacement(cx)),
 858                        Err(_) => {
 859                            self.query_contains_error = true;
 860                            self.active_match_index = None;
 861                            cx.notify();
 862                            return done_rx;
 863                        }
 864                    }
 865                } else {
 866                    match SearchQuery::text(
 867                        query,
 868                        self.search_options.contains(SearchOptions::WHOLE_WORD),
 869                        self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 870                        false,
 871                        Vec::new(),
 872                        Vec::new(),
 873                    ) {
 874                        Ok(query) => query.with_replacement(self.replacement(cx)),
 875                        Err(_) => {
 876                            self.query_contains_error = true;
 877                            self.active_match_index = None;
 878                            cx.notify();
 879                            return done_rx;
 880                        }
 881                    }
 882                }
 883                .into();
 884                self.active_search = Some(query.clone());
 885                let query_text = query.as_str().to_string();
 886
 887                let matches = active_searchable_item.find_matches(query, cx);
 888
 889                let active_searchable_item = active_searchable_item.downgrade();
 890                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
 891                    let matches = matches.await;
 892
 893                    this.update(&mut cx, |this, cx| {
 894                        if let Some(active_searchable_item) =
 895                            WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
 896                        {
 897                            this.searchable_items_with_matches
 898                                .insert(active_searchable_item.downgrade(), matches);
 899
 900                            this.update_match_index(cx);
 901                            this.search_history.add(query_text);
 902                            if !this.dismissed {
 903                                let matches = this
 904                                    .searchable_items_with_matches
 905                                    .get(&active_searchable_item.downgrade())
 906                                    .unwrap();
 907                                active_searchable_item.update_matches(matches, cx);
 908                                let _ = done_tx.send(());
 909                            }
 910                            cx.notify();
 911                        }
 912                    })
 913                    .log_err();
 914                }));
 915            }
 916        }
 917        done_rx
 918    }
 919
 920    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 921        let new_index = self
 922            .active_searchable_item
 923            .as_ref()
 924            .and_then(|searchable_item| {
 925                let matches = self
 926                    .searchable_items_with_matches
 927                    .get(&searchable_item.downgrade())?;
 928                searchable_item.active_match_index(matches, cx)
 929            });
 930        if new_index != self.active_match_index {
 931            self.active_match_index = new_index;
 932            cx.notify();
 933        }
 934    }
 935
 936    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
 937        if let Some(item) = self.active_searchable_item.as_ref() {
 938            let focus_handle = item.focus_handle(cx);
 939            cx.focus(&focus_handle);
 940            cx.stop_propagation();
 941        }
 942    }
 943
 944    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
 945        if let Some(new_query) = self.search_history.next().map(str::to_string) {
 946            let _ = self.search(&new_query, Some(self.search_options), cx);
 947        } else {
 948            self.search_history.reset_selection();
 949            let _ = self.search("", Some(self.search_options), cx);
 950        }
 951    }
 952
 953    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
 954        if self.query(cx).is_empty() {
 955            if let Some(new_query) = self.search_history.current().map(str::to_string) {
 956                let _ = self.search(&new_query, Some(self.search_options), cx);
 957                return;
 958            }
 959        }
 960
 961        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
 962            let _ = self.search(&new_query, Some(self.search_options), cx);
 963        }
 964    }
 965    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
 966        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
 967    }
 968    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
 969        if let Some(_) = &self.active_searchable_item {
 970            self.replace_enabled = !self.replace_enabled;
 971            if !self.replace_enabled {
 972                let handle = self.query_editor.focus_handle(cx);
 973                cx.focus(&handle);
 974            }
 975            cx.notify();
 976        }
 977    }
 978    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
 979        let mut should_propagate = true;
 980        if !self.dismissed && self.active_search.is_some() {
 981            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
 982                if let Some(query) = self.active_search.as_ref() {
 983                    if let Some(matches) = self
 984                        .searchable_items_with_matches
 985                        .get(&searchable_item.downgrade())
 986                    {
 987                        if let Some(active_index) = self.active_match_index {
 988                            let query = query
 989                                .as_ref()
 990                                .clone()
 991                                .with_replacement(self.replacement(cx));
 992                            searchable_item.replace(&matches[active_index], &query, cx);
 993                            self.select_next_match(&SelectNextMatch, cx);
 994                        }
 995                        should_propagate = false;
 996                        self.focus_editor(&FocusEditor, cx);
 997                    }
 998                }
 999            }
1000        }
1001        if !should_propagate {
1002            cx.stop_propagation();
1003        }
1004    }
1005    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
1006        if !self.dismissed && self.active_search.is_some() {
1007            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
1008                if let Some(query) = self.active_search.as_ref() {
1009                    if let Some(matches) = self
1010                        .searchable_items_with_matches
1011                        .get(&searchable_item.downgrade())
1012                    {
1013                        let query = query
1014                            .as_ref()
1015                            .clone()
1016                            .with_replacement(self.replacement(cx));
1017                        for m in matches {
1018                            searchable_item.replace(m, &query, cx);
1019                        }
1020                    }
1021                }
1022            }
1023        }
1024    }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use std::ops::Range;
1030
1031    use super::*;
1032    use editor::{DisplayPoint, Editor};
1033    use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
1034    use language::Buffer;
1035    use smol::stream::StreamExt as _;
1036    use unindent::Unindent as _;
1037
1038    fn init_globals(cx: &mut TestAppContext) {
1039        cx.update(|cx| {
1040            let store = settings::SettingsStore::test(cx);
1041            cx.set_global(store);
1042            editor::init(cx);
1043
1044            language::init(cx);
1045            theme::init(theme::LoadThemes::JustBase, cx);
1046        });
1047    }
1048    fn init_test(
1049        cx: &mut TestAppContext,
1050    ) -> (
1051        View<Editor>,
1052        View<BufferSearchBar>,
1053        &mut VisualTestContext<'_>,
1054    ) {
1055        init_globals(cx);
1056        let buffer = cx.new_model(|cx| {
1057            Buffer::new(
1058                0,
1059                cx.entity_id().as_u64(),
1060                r#"
1061                A regular expression (shortened as regex or regexp;[1] also referred to as
1062                rational expression[2][3]) is a sequence of characters that specifies a search
1063                pattern in text. Usually such patterns are used by string-searching algorithms
1064                for "find" or "find and replace" operations on strings, or for input validation.
1065                "#
1066                .unindent(),
1067            )
1068        });
1069        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1070        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1071
1072        let search_bar = cx.new_view(|cx| {
1073            let mut search_bar = BufferSearchBar::new(cx);
1074            search_bar.set_active_pane_item(Some(&editor), cx);
1075            search_bar.show(cx);
1076            search_bar
1077        });
1078
1079        (editor, search_bar, cx)
1080    }
1081
1082    #[gpui::test]
1083    async fn test_search_simple(cx: &mut TestAppContext) {
1084        let (editor, search_bar, cx) = init_test(cx);
1085        // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
1086        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1087            background_highlights
1088                .into_iter()
1089                .map(|(range, _)| range)
1090                .collect::<Vec<_>>()
1091        };
1092        // Search for a string that appears with different casing.
1093        // By default, search is case-insensitive.
1094        search_bar
1095            .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
1096            .await
1097            .unwrap();
1098        editor.update(cx, |editor, cx| {
1099            assert_eq!(
1100                display_points_of(editor.all_text_background_highlights(cx)),
1101                &[
1102                    DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
1103                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
1104                ]
1105            );
1106        });
1107
1108        // Switch to a case sensitive search.
1109        search_bar.update(cx, |search_bar, cx| {
1110            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
1111        });
1112        let mut editor_notifications = cx.notifications(&editor);
1113        editor_notifications.next().await;
1114        editor.update(cx, |editor, cx| {
1115            assert_eq!(
1116                display_points_of(editor.all_text_background_highlights(cx)),
1117                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1118            );
1119        });
1120
1121        // Search for a string that appears both as a whole word and
1122        // within other words. By default, all results are found.
1123        search_bar
1124            .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
1125            .await
1126            .unwrap();
1127        editor.update(cx, |editor, cx| {
1128            assert_eq!(
1129                display_points_of(editor.all_text_background_highlights(cx)),
1130                &[
1131                    DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
1132                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1133                    DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
1134                    DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
1135                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1136                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1137                    DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
1138                ]
1139            );
1140        });
1141
1142        // Switch to a whole word search.
1143        search_bar.update(cx, |search_bar, cx| {
1144            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
1145        });
1146        let mut editor_notifications = cx.notifications(&editor);
1147        editor_notifications.next().await;
1148        editor.update(cx, |editor, cx| {
1149            assert_eq!(
1150                display_points_of(editor.all_text_background_highlights(cx)),
1151                &[
1152                    DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
1153                    DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
1154                    DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
1155                ]
1156            );
1157        });
1158
1159        editor.update(cx, |editor, cx| {
1160            editor.change_selections(None, cx, |s| {
1161                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1162            });
1163        });
1164        search_bar.update(cx, |search_bar, cx| {
1165            assert_eq!(search_bar.active_match_index, Some(0));
1166            search_bar.select_next_match(&SelectNextMatch, cx);
1167            assert_eq!(
1168                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1169                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1170            );
1171        });
1172        search_bar.update(cx, |search_bar, _| {
1173            assert_eq!(search_bar.active_match_index, Some(0));
1174        });
1175
1176        search_bar.update(cx, |search_bar, cx| {
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        search_bar.update(cx, |search_bar, cx| {
1188            search_bar.select_next_match(&SelectNextMatch, cx);
1189            assert_eq!(
1190                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1191                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1192            );
1193        });
1194        search_bar.update(cx, |search_bar, _| {
1195            assert_eq!(search_bar.active_match_index, Some(2));
1196        });
1197
1198        search_bar.update(cx, |search_bar, cx| {
1199            search_bar.select_next_match(&SelectNextMatch, cx);
1200            assert_eq!(
1201                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1202                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1203            );
1204        });
1205        search_bar.update(cx, |search_bar, _| {
1206            assert_eq!(search_bar.active_match_index, Some(0));
1207        });
1208
1209        search_bar.update(cx, |search_bar, cx| {
1210            search_bar.select_prev_match(&SelectPrevMatch, cx);
1211            assert_eq!(
1212                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1213                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1214            );
1215        });
1216        search_bar.update(cx, |search_bar, _| {
1217            assert_eq!(search_bar.active_match_index, Some(2));
1218        });
1219
1220        search_bar.update(cx, |search_bar, cx| {
1221            search_bar.select_prev_match(&SelectPrevMatch, cx);
1222            assert_eq!(
1223                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1224                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1225            );
1226        });
1227        search_bar.update(cx, |search_bar, _| {
1228            assert_eq!(search_bar.active_match_index, Some(1));
1229        });
1230
1231        search_bar.update(cx, |search_bar, cx| {
1232            search_bar.select_prev_match(&SelectPrevMatch, cx);
1233            assert_eq!(
1234                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1235                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1236            );
1237        });
1238        search_bar.update(cx, |search_bar, _| {
1239            assert_eq!(search_bar.active_match_index, Some(0));
1240        });
1241
1242        // Park the cursor in between matches and ensure that going to the previous match selects
1243        // the closest match to the left.
1244        editor.update(cx, |editor, cx| {
1245            editor.change_selections(None, cx, |s| {
1246                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1247            });
1248        });
1249        search_bar.update(cx, |search_bar, cx| {
1250            assert_eq!(search_bar.active_match_index, Some(1));
1251            search_bar.select_prev_match(&SelectPrevMatch, cx);
1252            assert_eq!(
1253                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1254                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1255            );
1256        });
1257        search_bar.update(cx, |search_bar, _| {
1258            assert_eq!(search_bar.active_match_index, Some(0));
1259        });
1260
1261        // Park the cursor in between matches and ensure that going to the next match selects the
1262        // closest match to the right.
1263        editor.update(cx, |editor, cx| {
1264            editor.change_selections(None, cx, |s| {
1265                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1266            });
1267        });
1268        search_bar.update(cx, |search_bar, cx| {
1269            assert_eq!(search_bar.active_match_index, Some(1));
1270            search_bar.select_next_match(&SelectNextMatch, cx);
1271            assert_eq!(
1272                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1273                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
1274            );
1275        });
1276        search_bar.update(cx, |search_bar, _| {
1277            assert_eq!(search_bar.active_match_index, Some(1));
1278        });
1279
1280        // Park the cursor after the last match and ensure that going to the previous match selects
1281        // the last match.
1282        editor.update(cx, |editor, cx| {
1283            editor.change_selections(None, cx, |s| {
1284                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1285            });
1286        });
1287        search_bar.update(cx, |search_bar, cx| {
1288            assert_eq!(search_bar.active_match_index, Some(2));
1289            search_bar.select_prev_match(&SelectPrevMatch, cx);
1290            assert_eq!(
1291                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1292                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1293            );
1294        });
1295        search_bar.update(cx, |search_bar, _| {
1296            assert_eq!(search_bar.active_match_index, Some(2));
1297        });
1298
1299        // Park the cursor after the last match and ensure that going to the next match selects the
1300        // first match.
1301        editor.update(cx, |editor, cx| {
1302            editor.change_selections(None, cx, |s| {
1303                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
1304            });
1305        });
1306        search_bar.update(cx, |search_bar, cx| {
1307            assert_eq!(search_bar.active_match_index, Some(2));
1308            search_bar.select_next_match(&SelectNextMatch, cx);
1309            assert_eq!(
1310                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1311                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
1312            );
1313        });
1314        search_bar.update(cx, |search_bar, _| {
1315            assert_eq!(search_bar.active_match_index, Some(0));
1316        });
1317
1318        // Park the cursor before the first match and ensure that going to the previous match
1319        // selects the last match.
1320        editor.update(cx, |editor, cx| {
1321            editor.change_selections(None, cx, |s| {
1322                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
1323            });
1324        });
1325        search_bar.update(cx, |search_bar, cx| {
1326            assert_eq!(search_bar.active_match_index, Some(0));
1327            search_bar.select_prev_match(&SelectPrevMatch, cx);
1328            assert_eq!(
1329                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1330                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
1331            );
1332        });
1333        search_bar.update(cx, |search_bar, _| {
1334            assert_eq!(search_bar.active_match_index, Some(2));
1335        });
1336    }
1337
1338    #[gpui::test]
1339    async fn test_search_option_handling(cx: &mut TestAppContext) {
1340        let (editor, search_bar, cx) = init_test(cx);
1341
1342        // show with options should make current search case sensitive
1343        search_bar
1344            .update(cx, |search_bar, cx| {
1345                search_bar.show(cx);
1346                search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
1347            })
1348            .await
1349            .unwrap();
1350        // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
1351        let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
1352            background_highlights
1353                .into_iter()
1354                .map(|(range, _)| range)
1355                .collect::<Vec<_>>()
1356        };
1357        editor.update(cx, |editor, cx| {
1358            assert_eq!(
1359                display_points_of(editor.all_text_background_highlights(cx)),
1360                &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
1361            );
1362        });
1363
1364        // search_suggested should restore default options
1365        search_bar.update(cx, |search_bar, cx| {
1366            search_bar.search_suggested(cx);
1367            assert_eq!(search_bar.search_options, SearchOptions::NONE)
1368        });
1369
1370        // toggling a search option should update the defaults
1371        search_bar
1372            .update(cx, |search_bar, cx| {
1373                search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
1374            })
1375            .await
1376            .unwrap();
1377        search_bar.update(cx, |search_bar, cx| {
1378            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
1379        });
1380        let mut editor_notifications = cx.notifications(&editor);
1381        editor_notifications.next().await;
1382        editor.update(cx, |editor, cx| {
1383            assert_eq!(
1384                display_points_of(editor.all_text_background_highlights(cx)),
1385                &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
1386            );
1387        });
1388
1389        // defaults should still include whole word
1390        search_bar.update(cx, |search_bar, cx| {
1391            search_bar.search_suggested(cx);
1392            assert_eq!(
1393                search_bar.search_options,
1394                SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
1395            )
1396        });
1397    }
1398
1399    #[gpui::test]
1400    async fn test_search_select_all_matches(cx: &mut TestAppContext) {
1401        init_globals(cx);
1402        let buffer_text = r#"
1403        A regular expression (shortened as regex or regexp;[1] also referred to as
1404        rational expression[2][3]) is a sequence of characters that specifies a search
1405        pattern in text. Usually such patterns are used by string-searching algorithms
1406        for "find" or "find and replace" operations on strings, or for input validation.
1407        "#
1408        .unindent();
1409        let expected_query_matches_count = buffer_text
1410            .chars()
1411            .filter(|c| c.to_ascii_lowercase() == 'a')
1412            .count();
1413        assert!(
1414            expected_query_matches_count > 1,
1415            "Should pick a query with multiple results"
1416        );
1417        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1418        let window = cx.add_window(|_| EmptyView {});
1419
1420        let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
1421
1422        let search_bar = window.build_view(cx, |cx| {
1423            let mut search_bar = BufferSearchBar::new(cx);
1424            search_bar.set_active_pane_item(Some(&editor), cx);
1425            search_bar.show(cx);
1426            search_bar
1427        });
1428
1429        window
1430            .update(cx, |_, cx| {
1431                search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1432            })
1433            .unwrap()
1434            .await
1435            .unwrap();
1436        let initial_selections = window
1437            .update(cx, |_, cx| {
1438                search_bar.update(cx, |search_bar, cx| {
1439                    let handle = search_bar.query_editor.focus_handle(cx);
1440                    cx.focus(&handle);
1441                    search_bar.activate_current_match(cx);
1442                });
1443                assert!(
1444                    !editor.read(cx).is_focused(cx),
1445                    "Initially, the editor should not be focused"
1446                );
1447                let initial_selections = editor.update(cx, |editor, cx| {
1448                    let initial_selections = editor.selections.display_ranges(cx);
1449                    assert_eq!(
1450                        initial_selections.len(), 1,
1451                        "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
1452                    );
1453                    initial_selections
1454                });
1455                search_bar.update(cx, |search_bar, cx| {
1456                    assert_eq!(search_bar.active_match_index, Some(0));
1457                    let handle = search_bar.query_editor.focus_handle(cx);
1458                    cx.focus(&handle);
1459                    search_bar.select_all_matches(&SelectAllMatches, cx);
1460                });
1461                assert!(
1462                    editor.read(cx).is_focused(cx),
1463                    "Should focus editor after successful SelectAllMatches"
1464                );
1465                search_bar.update(cx, |search_bar, cx| {
1466                    let all_selections =
1467                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1468                    assert_eq!(
1469                        all_selections.len(),
1470                        expected_query_matches_count,
1471                        "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1472                    );
1473                    assert_eq!(
1474                        search_bar.active_match_index,
1475                        Some(0),
1476                        "Match index should not change after selecting all matches"
1477                    );
1478                });
1479
1480                search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
1481                initial_selections
1482            }).unwrap();
1483
1484        window
1485            .update(cx, |_, cx| {
1486                assert!(
1487                    editor.read(cx).is_focused(cx),
1488                    "Should still have editor focused after SelectNextMatch"
1489                );
1490                search_bar.update(cx, |search_bar, cx| {
1491                    let all_selections =
1492                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1493                    assert_eq!(
1494                        all_selections.len(),
1495                        1,
1496                        "On next match, should deselect items and select the next match"
1497                    );
1498                    assert_ne!(
1499                        all_selections, initial_selections,
1500                        "Next match should be different from the first selection"
1501                    );
1502                    assert_eq!(
1503                        search_bar.active_match_index,
1504                        Some(1),
1505                        "Match index should be updated to the next one"
1506                    );
1507                    let handle = search_bar.query_editor.focus_handle(cx);
1508                    cx.focus(&handle);
1509                    search_bar.select_all_matches(&SelectAllMatches, cx);
1510                });
1511            })
1512            .unwrap();
1513        window
1514            .update(cx, |_, cx| {
1515                assert!(
1516                    editor.read(cx).is_focused(cx),
1517                    "Should focus editor after successful SelectAllMatches"
1518                );
1519                search_bar.update(cx, |search_bar, cx| {
1520                    let all_selections =
1521                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1522                    assert_eq!(
1523                    all_selections.len(),
1524                    expected_query_matches_count,
1525                    "Should select all `a` characters in the buffer, but got: {all_selections:?}"
1526                );
1527                    assert_eq!(
1528                        search_bar.active_match_index,
1529                        Some(1),
1530                        "Match index should not change after selecting all matches"
1531                    );
1532                });
1533                search_bar.update(cx, |search_bar, cx| {
1534                    search_bar.select_prev_match(&SelectPrevMatch, cx);
1535                });
1536            })
1537            .unwrap();
1538        let last_match_selections = window
1539            .update(cx, |_, cx| {
1540                assert!(
1541                    editor.read(cx).is_focused(&cx),
1542                    "Should still have editor focused after SelectPrevMatch"
1543                );
1544
1545                search_bar.update(cx, |search_bar, cx| {
1546                    let all_selections =
1547                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1548                    assert_eq!(
1549                        all_selections.len(),
1550                        1,
1551                        "On previous match, should deselect items and select the previous item"
1552                    );
1553                    assert_eq!(
1554                        all_selections, initial_selections,
1555                        "Previous match should be the same as the first selection"
1556                    );
1557                    assert_eq!(
1558                        search_bar.active_match_index,
1559                        Some(0),
1560                        "Match index should be updated to the previous one"
1561                    );
1562                    all_selections
1563                })
1564            })
1565            .unwrap();
1566
1567        window
1568            .update(cx, |_, cx| {
1569                search_bar.update(cx, |search_bar, cx| {
1570                    let handle = search_bar.query_editor.focus_handle(cx);
1571                    cx.focus(&handle);
1572                    search_bar.search("abas_nonexistent_match", None, cx)
1573                })
1574            })
1575            .unwrap()
1576            .await
1577            .unwrap();
1578        window
1579            .update(cx, |_, cx| {
1580                search_bar.update(cx, |search_bar, cx| {
1581                    search_bar.select_all_matches(&SelectAllMatches, cx);
1582                });
1583                assert!(
1584                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
1585                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
1586                );
1587                search_bar.update(cx, |search_bar, cx| {
1588                    let all_selections =
1589                        editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
1590                    assert_eq!(
1591                        all_selections, last_match_selections,
1592                        "Should not select anything new if there are no matches"
1593                    );
1594                    assert!(
1595                        search_bar.active_match_index.is_none(),
1596                        "For no matches, there should be no active match index"
1597                    );
1598                });
1599            })
1600            .unwrap();
1601    }
1602
1603    #[gpui::test]
1604    async fn test_search_query_history(cx: &mut TestAppContext) {
1605        //crate::project_search::tests::init_test(cx);
1606        init_globals(cx);
1607        let buffer_text = r#"
1608        A regular expression (shortened as regex or regexp;[1] also referred to as
1609        rational expression[2][3]) is a sequence of characters that specifies a search
1610        pattern in text. Usually such patterns are used by string-searching algorithms
1611        for "find" or "find and replace" operations on strings, or for input validation.
1612        "#
1613        .unindent();
1614        let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
1615        let (_, cx) = cx.add_window_view(|_| EmptyView {});
1616
1617        let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
1618
1619        let search_bar = cx.new_view(|cx| {
1620            let mut search_bar = BufferSearchBar::new(cx);
1621            search_bar.set_active_pane_item(Some(&editor), cx);
1622            search_bar.show(cx);
1623            search_bar
1624        });
1625
1626        // Add 3 search items into the history.
1627        search_bar
1628            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
1629            .await
1630            .unwrap();
1631        search_bar
1632            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
1633            .await
1634            .unwrap();
1635        search_bar
1636            .update(cx, |search_bar, cx| {
1637                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
1638            })
1639            .await
1640            .unwrap();
1641        // Ensure that the latest search is active.
1642        search_bar.update(cx, |search_bar, cx| {
1643            assert_eq!(search_bar.query(cx), "c");
1644            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1645        });
1646
1647        // Next history query after the latest should set the query to the empty string.
1648        search_bar.update(cx, |search_bar, cx| {
1649            search_bar.next_history_query(&NextHistoryQuery, cx);
1650        });
1651        search_bar.update(cx, |search_bar, cx| {
1652            assert_eq!(search_bar.query(cx), "");
1653            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1654        });
1655        search_bar.update(cx, |search_bar, cx| {
1656            search_bar.next_history_query(&NextHistoryQuery, cx);
1657        });
1658        search_bar.update(cx, |search_bar, cx| {
1659            assert_eq!(search_bar.query(cx), "");
1660            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1661        });
1662
1663        // First previous query for empty current query should set the query to the latest.
1664        search_bar.update(cx, |search_bar, cx| {
1665            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1666        });
1667        search_bar.update(cx, |search_bar, cx| {
1668            assert_eq!(search_bar.query(cx), "c");
1669            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1670        });
1671
1672        // Further previous items should go over the history in reverse order.
1673        search_bar.update(cx, |search_bar, cx| {
1674            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1675        });
1676        search_bar.update(cx, |search_bar, cx| {
1677            assert_eq!(search_bar.query(cx), "b");
1678            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1679        });
1680
1681        // Previous items should never go behind the first history item.
1682        search_bar.update(cx, |search_bar, cx| {
1683            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1684        });
1685        search_bar.update(cx, |search_bar, cx| {
1686            assert_eq!(search_bar.query(cx), "a");
1687            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1688        });
1689        search_bar.update(cx, |search_bar, cx| {
1690            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1691        });
1692        search_bar.update(cx, |search_bar, cx| {
1693            assert_eq!(search_bar.query(cx), "a");
1694            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1695        });
1696
1697        // Next items should go over the history in the original order.
1698        search_bar.update(cx, |search_bar, cx| {
1699            search_bar.next_history_query(&NextHistoryQuery, cx);
1700        });
1701        search_bar.update(cx, |search_bar, cx| {
1702            assert_eq!(search_bar.query(cx), "b");
1703            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
1704        });
1705
1706        search_bar
1707            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
1708            .await
1709            .unwrap();
1710        search_bar.update(cx, |search_bar, cx| {
1711            assert_eq!(search_bar.query(cx), "ba");
1712            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1713        });
1714
1715        // New search input should add another entry to history and move the selection to the end of the history.
1716        search_bar.update(cx, |search_bar, cx| {
1717            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1718        });
1719        search_bar.update(cx, |search_bar, cx| {
1720            assert_eq!(search_bar.query(cx), "c");
1721            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1722        });
1723        search_bar.update(cx, |search_bar, cx| {
1724            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1725        });
1726        search_bar.update(cx, |search_bar, cx| {
1727            assert_eq!(search_bar.query(cx), "b");
1728            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1729        });
1730        search_bar.update(cx, |search_bar, cx| {
1731            search_bar.next_history_query(&NextHistoryQuery, cx);
1732        });
1733        search_bar.update(cx, |search_bar, cx| {
1734            assert_eq!(search_bar.query(cx), "c");
1735            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1736        });
1737        search_bar.update(cx, |search_bar, cx| {
1738            search_bar.next_history_query(&NextHistoryQuery, cx);
1739        });
1740        search_bar.update(cx, |search_bar, cx| {
1741            assert_eq!(search_bar.query(cx), "ba");
1742            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1743        });
1744        search_bar.update(cx, |search_bar, cx| {
1745            search_bar.next_history_query(&NextHistoryQuery, cx);
1746        });
1747        search_bar.update(cx, |search_bar, cx| {
1748            assert_eq!(search_bar.query(cx), "");
1749            assert_eq!(search_bar.search_options, SearchOptions::NONE);
1750        });
1751    }
1752
1753    #[gpui::test]
1754    async fn test_replace_simple(cx: &mut TestAppContext) {
1755        let (editor, search_bar, cx) = init_test(cx);
1756
1757        search_bar
1758            .update(cx, |search_bar, cx| {
1759                search_bar.search("expression", None, cx)
1760            })
1761            .await
1762            .unwrap();
1763
1764        search_bar.update(cx, |search_bar, cx| {
1765            search_bar.replacement_editor.update(cx, |editor, cx| {
1766                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
1767                editor.set_text("expr$1", cx);
1768            });
1769            search_bar.replace_all(&ReplaceAll, cx)
1770        });
1771        assert_eq!(
1772            editor.update(cx, |this, cx| { this.text(cx) }),
1773            r#"
1774        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
1775        rational expr$1[2][3]) is a sequence of characters that specifies a search
1776        pattern in text. Usually such patterns are used by string-searching algorithms
1777        for "find" or "find and replace" operations on strings, or for input validation.
1778        "#
1779            .unindent()
1780        );
1781
1782        // Search for word boundaries and replace just a single one.
1783        search_bar
1784            .update(cx, |search_bar, cx| {
1785                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
1786            })
1787            .await
1788            .unwrap();
1789
1790        search_bar.update(cx, |search_bar, cx| {
1791            search_bar.replacement_editor.update(cx, |editor, cx| {
1792                editor.set_text("banana", cx);
1793            });
1794            search_bar.replace_next(&ReplaceNext, cx)
1795        });
1796        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
1797        assert_eq!(
1798            editor.update(cx, |this, cx| { this.text(cx) }),
1799            r#"
1800        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
1801        rational expr$1[2][3]) is a sequence of characters that specifies a search
1802        pattern in text. Usually such patterns are used by string-searching algorithms
1803        for "find" or "find and replace" operations on strings, or for input validation.
1804        "#
1805            .unindent()
1806        );
1807        // Let's turn on regex mode.
1808        search_bar
1809            .update(cx, |search_bar, cx| {
1810                search_bar.activate_search_mode(SearchMode::Regex, cx);
1811                search_bar.search("\\[([^\\]]+)\\]", None, cx)
1812            })
1813            .await
1814            .unwrap();
1815        search_bar.update(cx, |search_bar, cx| {
1816            search_bar.replacement_editor.update(cx, |editor, cx| {
1817                editor.set_text("${1}number", cx);
1818            });
1819            search_bar.replace_all(&ReplaceAll, cx)
1820        });
1821        assert_eq!(
1822            editor.update(cx, |this, cx| { this.text(cx) }),
1823            r#"
1824        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1825        rational expr$12number3number) is a sequence of characters that specifies a search
1826        pattern in text. Usually such patterns are used by string-searching algorithms
1827        for "find" or "find and replace" operations on strings, or for input validation.
1828        "#
1829            .unindent()
1830        );
1831        // Now with a whole-word twist.
1832        search_bar
1833            .update(cx, |search_bar, cx| {
1834                search_bar.activate_search_mode(SearchMode::Regex, cx);
1835                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
1836            })
1837            .await
1838            .unwrap();
1839        search_bar.update(cx, |search_bar, cx| {
1840            search_bar.replacement_editor.update(cx, |editor, cx| {
1841                editor.set_text("things", cx);
1842            });
1843            search_bar.replace_all(&ReplaceAll, cx)
1844        });
1845        // The only word affected by this edit should be `algorithms`, even though there's a bunch
1846        // of words in this text that would match this regex if not for WHOLE_WORD.
1847        assert_eq!(
1848            editor.update(cx, |this, cx| { this.text(cx) }),
1849            r#"
1850        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
1851        rational expr$12number3number) is a sequence of characters that specifies a search
1852        pattern in text. Usually such patterns are used by string-searching things
1853        for "find" or "find and replace" operations on strings, or for input validation.
1854        "#
1855            .unindent()
1856        );
1857    }
1858}