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