project_search.rs

   1use crate::{
   2    BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
   3    SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch,
   4    ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,
   5    buffer_search::Deploy,
   6    search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
   7};
   8use anyhow::Context as _;
   9use collections::HashMap;
  10use editor::{
  11    Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey,
  12    SelectionEffects,
  13    actions::{Backtab, SelectAll, Tab},
  14    items::active_match_index,
  15    multibuffer_context_lines,
  16};
  17use futures::{StreamExt, stream::FuturesOrdered};
  18use gpui::{
  19    Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle,
  20    Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point,
  21    Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions,
  22    div,
  23};
  24use language::{Buffer, Language};
  25use menu::Confirm;
  26use project::{
  27    Project, ProjectPath,
  28    search::{SearchInputKind, SearchQuery},
  29    search_history::SearchHistoryCursor,
  30};
  31use settings::Settings;
  32use std::{
  33    any::{Any, TypeId},
  34    mem,
  35    ops::{Not, Range},
  36    pin::pin,
  37    sync::Arc,
  38};
  39use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth};
  40use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath};
  41use workspace::{
  42    DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
  43    ToolbarItemView, Workspace, WorkspaceId,
  44    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions},
  45    searchable::{Direction, SearchableItem, SearchableItemHandle},
  46};
  47
  48actions!(
  49    project_search,
  50    [
  51        /// Searches in a new project search tab.
  52        SearchInNew,
  53        /// Toggles focus between the search bar and the search results.
  54        ToggleFocus,
  55        /// Moves to the next input field.
  56        NextField,
  57        /// Toggles the search filters panel.
  58        ToggleFilters
  59    ]
  60);
  61
  62#[derive(Default)]
  63struct ActiveSettings(HashMap<WeakEntity<Project>, ProjectSearchSettings>);
  64
  65impl Global for ActiveSettings {}
  66
  67pub fn init(cx: &mut App) {
  68    cx.set_global(ActiveSettings::default());
  69    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
  70        register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| {
  71            search_bar.focus_search(window, cx);
  72        });
  73        register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| {
  74            search_bar.focus_search(window, cx);
  75        });
  76        register_workspace_action(
  77            workspace,
  78            move |search_bar, _: &ToggleFilters, window, cx| {
  79                search_bar.toggle_filters(window, cx);
  80            },
  81        );
  82        register_workspace_action(
  83            workspace,
  84            move |search_bar, _: &ToggleCaseSensitive, window, cx| {
  85                search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
  86            },
  87        );
  88        register_workspace_action(
  89            workspace,
  90            move |search_bar, _: &ToggleWholeWord, window, cx| {
  91                search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
  92            },
  93        );
  94        register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| {
  95            search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
  96        });
  97        register_workspace_action(
  98            workspace,
  99            move |search_bar, action: &ToggleReplace, window, cx| {
 100                search_bar.toggle_replace(action, window, cx)
 101            },
 102        );
 103        register_workspace_action(
 104            workspace,
 105            move |search_bar, action: &SelectPreviousMatch, window, cx| {
 106                search_bar.select_prev_match(action, window, cx)
 107            },
 108        );
 109        register_workspace_action(
 110            workspace,
 111            move |search_bar, action: &SelectNextMatch, window, cx| {
 112                search_bar.select_next_match(action, window, cx)
 113            },
 114        );
 115
 116        // Only handle search_in_new if there is a search present
 117        register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| {
 118            ProjectSearchView::search_in_new(workspace, action, window, cx)
 119        });
 120
 121        register_workspace_action_for_present_search(
 122            workspace,
 123            |workspace, _: &menu::Cancel, window, cx| {
 124                if let Some(project_search_bar) = workspace
 125                    .active_pane()
 126                    .read(cx)
 127                    .toolbar()
 128                    .read(cx)
 129                    .item_of_type::<ProjectSearchBar>()
 130                {
 131                    project_search_bar.update(cx, |project_search_bar, cx| {
 132                        let search_is_focused = project_search_bar
 133                            .active_project_search
 134                            .as_ref()
 135                            .is_some_and(|search_view| {
 136                                search_view
 137                                    .read(cx)
 138                                    .query_editor
 139                                    .read(cx)
 140                                    .focus_handle(cx)
 141                                    .is_focused(window)
 142                            });
 143                        if search_is_focused {
 144                            project_search_bar.move_focus_to_results(window, cx);
 145                        } else {
 146                            project_search_bar.focus_search(window, cx)
 147                        }
 148                    });
 149                } else {
 150                    cx.propagate();
 151                }
 152            },
 153        );
 154
 155        // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor.
 156        workspace.register_action(move |workspace, action: &DeploySearch, window, cx| {
 157            if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
 158                cx.propagate();
 159                return;
 160            }
 161            ProjectSearchView::deploy_search(workspace, action, window, cx);
 162            cx.notify();
 163        });
 164        workspace.register_action(move |workspace, action: &NewSearch, window, cx| {
 165            if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
 166                cx.propagate();
 167                return;
 168            }
 169            ProjectSearchView::new_search(workspace, action, window, cx);
 170            cx.notify();
 171        });
 172    })
 173    .detach();
 174}
 175
 176fn contains_uppercase(str: &str) -> bool {
 177    str.chars().any(|c| c.is_uppercase())
 178}
 179
 180pub struct ProjectSearch {
 181    project: Entity<Project>,
 182    excerpts: Entity<MultiBuffer>,
 183    pending_search: Option<Task<Option<()>>>,
 184    match_ranges: Vec<Range<Anchor>>,
 185    active_query: Option<SearchQuery>,
 186    last_search_query_text: Option<String>,
 187    search_id: usize,
 188    no_results: Option<bool>,
 189    limit_reached: bool,
 190    search_history_cursor: SearchHistoryCursor,
 191    search_included_history_cursor: SearchHistoryCursor,
 192    search_excluded_history_cursor: SearchHistoryCursor,
 193}
 194
 195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 196enum InputPanel {
 197    Query,
 198    Replacement,
 199    Exclude,
 200    Include,
 201}
 202
 203pub struct ProjectSearchView {
 204    workspace: WeakEntity<Workspace>,
 205    focus_handle: FocusHandle,
 206    entity: Entity<ProjectSearch>,
 207    query_editor: Entity<Editor>,
 208    replacement_editor: Entity<Editor>,
 209    results_editor: Entity<Editor>,
 210    search_options: SearchOptions,
 211    panels_with_errors: HashMap<InputPanel, String>,
 212    active_match_index: Option<usize>,
 213    search_id: usize,
 214    included_files_editor: Entity<Editor>,
 215    excluded_files_editor: Entity<Editor>,
 216    filters_enabled: bool,
 217    replace_enabled: bool,
 218    included_opened_only: bool,
 219    regex_language: Option<Arc<Language>>,
 220    _subscriptions: Vec<Subscription>,
 221}
 222
 223#[derive(Debug, Clone)]
 224pub struct ProjectSearchSettings {
 225    search_options: SearchOptions,
 226    filters_enabled: bool,
 227}
 228
 229pub struct ProjectSearchBar {
 230    active_project_search: Option<Entity<ProjectSearchView>>,
 231    subscription: Option<Subscription>,
 232}
 233
 234impl ProjectSearch {
 235    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
 236        let capability = project.read(cx).capability();
 237
 238        Self {
 239            project,
 240            excerpts: cx.new(|_| MultiBuffer::new(capability)),
 241            pending_search: Default::default(),
 242            match_ranges: Default::default(),
 243            active_query: None,
 244            last_search_query_text: None,
 245            search_id: 0,
 246            no_results: None,
 247            limit_reached: false,
 248            search_history_cursor: Default::default(),
 249            search_included_history_cursor: Default::default(),
 250            search_excluded_history_cursor: Default::default(),
 251        }
 252    }
 253
 254    fn clone(&self, cx: &mut Context<Self>) -> Entity<Self> {
 255        cx.new(|cx| Self {
 256            project: self.project.clone(),
 257            excerpts: self
 258                .excerpts
 259                .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))),
 260            pending_search: Default::default(),
 261            match_ranges: self.match_ranges.clone(),
 262            active_query: self.active_query.clone(),
 263            last_search_query_text: self.last_search_query_text.clone(),
 264            search_id: self.search_id,
 265            no_results: self.no_results,
 266            limit_reached: self.limit_reached,
 267            search_history_cursor: self.search_history_cursor.clone(),
 268            search_included_history_cursor: self.search_included_history_cursor.clone(),
 269            search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),
 270        })
 271    }
 272    fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor {
 273        match kind {
 274            SearchInputKind::Query => &self.search_history_cursor,
 275            SearchInputKind::Include => &self.search_included_history_cursor,
 276            SearchInputKind::Exclude => &self.search_excluded_history_cursor,
 277        }
 278    }
 279    fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor {
 280        match kind {
 281            SearchInputKind::Query => &mut self.search_history_cursor,
 282            SearchInputKind::Include => &mut self.search_included_history_cursor,
 283            SearchInputKind::Exclude => &mut self.search_excluded_history_cursor,
 284        }
 285    }
 286
 287    fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) {
 288        let search = self.project.update(cx, |project, cx| {
 289            project
 290                .search_history_mut(SearchInputKind::Query)
 291                .add(&mut self.search_history_cursor, query.as_str().to_string());
 292            let included = query.as_inner().files_to_include().sources().join(",");
 293            if !included.is_empty() {
 294                project
 295                    .search_history_mut(SearchInputKind::Include)
 296                    .add(&mut self.search_included_history_cursor, included);
 297            }
 298            let excluded = query.as_inner().files_to_exclude().sources().join(",");
 299            if !excluded.is_empty() {
 300                project
 301                    .search_history_mut(SearchInputKind::Exclude)
 302                    .add(&mut self.search_excluded_history_cursor, excluded);
 303            }
 304            project.search(query.clone(), cx)
 305        });
 306        self.last_search_query_text = Some(query.as_str().to_string());
 307        self.search_id += 1;
 308        self.active_query = Some(query);
 309        self.match_ranges.clear();
 310        self.pending_search = Some(cx.spawn(async move |project_search, cx| {
 311            let mut matches = pin!(search.ready_chunks(1024));
 312            project_search
 313                .update(cx, |project_search, cx| {
 314                    project_search.match_ranges.clear();
 315                    project_search
 316                        .excerpts
 317                        .update(cx, |excerpts, cx| excerpts.clear(cx));
 318                    project_search.no_results = Some(true);
 319                    project_search.limit_reached = false;
 320                })
 321                .ok()?;
 322
 323            let mut limit_reached = false;
 324            while let Some(results) = matches.next().await {
 325                let mut buffers_with_ranges = Vec::with_capacity(results.len());
 326                for result in results {
 327                    match result {
 328                        project::search::SearchResult::Buffer { buffer, ranges } => {
 329                            buffers_with_ranges.push((buffer, ranges));
 330                        }
 331                        project::search::SearchResult::LimitReached => {
 332                            limit_reached = true;
 333                        }
 334                    }
 335                }
 336
 337                let mut new_ranges = project_search
 338                    .update(cx, |project_search, cx| {
 339                        project_search.excerpts.update(cx, |excerpts, cx| {
 340                            buffers_with_ranges
 341                                .into_iter()
 342                                .map(|(buffer, ranges)| {
 343                                    excerpts.set_anchored_excerpts_for_path(
 344                                        PathKey::for_buffer(&buffer, cx),
 345                                        buffer,
 346                                        ranges,
 347                                        multibuffer_context_lines(cx),
 348                                        cx,
 349                                    )
 350                                })
 351                                .collect::<FuturesOrdered<_>>()
 352                        })
 353                    })
 354                    .ok()?;
 355
 356                while let Some(new_ranges) = new_ranges.next().await {
 357                    project_search
 358                        .update(cx, |project_search, cx| {
 359                            project_search.match_ranges.extend(new_ranges);
 360                            cx.notify();
 361                        })
 362                        .ok()?;
 363                }
 364            }
 365
 366            project_search
 367                .update(cx, |project_search, cx| {
 368                    if !project_search.match_ranges.is_empty() {
 369                        project_search.no_results = Some(false);
 370                    }
 371                    project_search.limit_reached = limit_reached;
 372                    project_search.pending_search.take();
 373                    cx.notify();
 374                })
 375                .ok()?;
 376
 377            None
 378        }));
 379        cx.notify();
 380    }
 381}
 382
 383#[derive(Clone, Debug, PartialEq, Eq)]
 384pub enum ViewEvent {
 385    UpdateTab,
 386    Activate,
 387    EditorEvent(editor::EditorEvent),
 388    Dismiss,
 389}
 390
 391impl EventEmitter<ViewEvent> for ProjectSearchView {}
 392
 393impl Render for ProjectSearchView {
 394    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 395        if self.has_matches() {
 396            div()
 397                .flex_1()
 398                .size_full()
 399                .track_focus(&self.focus_handle(cx))
 400                .child(self.results_editor.clone())
 401        } else {
 402            let model = self.entity.read(cx);
 403            let has_no_results = model.no_results.unwrap_or(false);
 404            let is_search_underway = model.pending_search.is_some();
 405
 406            let heading_text = if is_search_underway {
 407                "Searching…"
 408            } else if has_no_results {
 409                "No Results"
 410            } else {
 411                "Search All Files"
 412            };
 413
 414            let heading_text = div()
 415                .justify_center()
 416                .child(Label::new(heading_text).size(LabelSize::Large));
 417
 418            let page_content: Option<AnyElement> = if let Some(no_results) = model.no_results {
 419                if model.pending_search.is_none() && no_results {
 420                    Some(
 421                        Label::new("No results found in this project for the provided query")
 422                            .size(LabelSize::Small)
 423                            .into_any_element(),
 424                    )
 425                } else {
 426                    None
 427                }
 428            } else {
 429                Some(self.landing_text_minor(window, cx).into_any_element())
 430            };
 431
 432            let page_content = page_content.map(|text| div().child(text));
 433
 434            h_flex()
 435                .size_full()
 436                .items_center()
 437                .justify_center()
 438                .overflow_hidden()
 439                .bg(cx.theme().colors().editor_background)
 440                .track_focus(&self.focus_handle(cx))
 441                .child(
 442                    v_flex()
 443                        .id("project-search-landing-page")
 444                        .overflow_y_scroll()
 445                        .gap_1()
 446                        .child(heading_text)
 447                        .children(page_content),
 448                )
 449        }
 450    }
 451}
 452
 453impl Focusable for ProjectSearchView {
 454    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 455        self.focus_handle.clone()
 456    }
 457}
 458
 459impl Item for ProjectSearchView {
 460    type Event = ViewEvent;
 461    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
 462        let query_text = self.query_editor.read(cx).text(cx);
 463
 464        query_text
 465            .is_empty()
 466            .not()
 467            .then(|| query_text.into())
 468            .or_else(|| Some("Project Search".into()))
 469    }
 470
 471    fn act_as_type<'a>(
 472        &'a self,
 473        type_id: TypeId,
 474        self_handle: &'a Entity<Self>,
 475        _: &'a App,
 476    ) -> Option<AnyView> {
 477        if type_id == TypeId::of::<Self>() {
 478            Some(self_handle.clone().into())
 479        } else if type_id == TypeId::of::<Editor>() {
 480            Some(self.results_editor.clone().into())
 481        } else {
 482            None
 483        }
 484    }
 485    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 486        Some(Box::new(self.results_editor.clone()))
 487    }
 488
 489    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 490        self.results_editor
 491            .update(cx, |editor, cx| editor.deactivated(window, cx));
 492    }
 493
 494    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 495        Some(Icon::new(IconName::MagnifyingGlass))
 496    }
 497
 498    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
 499        let last_query: Option<SharedString> = self
 500            .entity
 501            .read(cx)
 502            .last_search_query_text
 503            .as_ref()
 504            .map(|query| {
 505                let query = query.replace('\n', "");
 506                let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN);
 507                query_text.into()
 508            });
 509
 510        last_query
 511            .filter(|query| !query.is_empty())
 512            .unwrap_or_else(|| "Project Search".into())
 513    }
 514
 515    fn telemetry_event_text(&self) -> Option<&'static str> {
 516        Some("Project Search Opened")
 517    }
 518
 519    fn for_each_project_item(
 520        &self,
 521        cx: &App,
 522        f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
 523    ) {
 524        self.results_editor.for_each_project_item(cx, f)
 525    }
 526
 527    fn can_save(&self, _: &App) -> bool {
 528        true
 529    }
 530
 531    fn is_dirty(&self, cx: &App) -> bool {
 532        self.results_editor.read(cx).is_dirty(cx)
 533    }
 534
 535    fn has_conflict(&self, cx: &App) -> bool {
 536        self.results_editor.read(cx).has_conflict(cx)
 537    }
 538
 539    fn save(
 540        &mut self,
 541        options: SaveOptions,
 542        project: Entity<Project>,
 543        window: &mut Window,
 544        cx: &mut Context<Self>,
 545    ) -> Task<anyhow::Result<()>> {
 546        self.results_editor
 547            .update(cx, |editor, cx| editor.save(options, project, window, cx))
 548    }
 549
 550    fn save_as(
 551        &mut self,
 552        _: Entity<Project>,
 553        _: ProjectPath,
 554        _window: &mut Window,
 555        _: &mut Context<Self>,
 556    ) -> Task<anyhow::Result<()>> {
 557        unreachable!("save_as should not have been called")
 558    }
 559
 560    fn reload(
 561        &mut self,
 562        project: Entity<Project>,
 563        window: &mut Window,
 564        cx: &mut Context<Self>,
 565    ) -> Task<anyhow::Result<()>> {
 566        self.results_editor
 567            .update(cx, |editor, cx| editor.reload(project, window, cx))
 568    }
 569
 570    fn clone_on_split(
 571        &self,
 572        _workspace_id: Option<WorkspaceId>,
 573        window: &mut Window,
 574        cx: &mut Context<Self>,
 575    ) -> Option<Entity<Self>>
 576    where
 577        Self: Sized,
 578    {
 579        let model = self.entity.update(cx, |model, cx| model.clone(cx));
 580        Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None)))
 581    }
 582
 583    fn added_to_workspace(
 584        &mut self,
 585        workspace: &mut Workspace,
 586        window: &mut Window,
 587        cx: &mut Context<Self>,
 588    ) {
 589        self.results_editor.update(cx, |editor, cx| {
 590            editor.added_to_workspace(workspace, window, cx)
 591        });
 592    }
 593
 594    fn set_nav_history(
 595        &mut self,
 596        nav_history: ItemNavHistory,
 597        _: &mut Window,
 598        cx: &mut Context<Self>,
 599    ) {
 600        self.results_editor.update(cx, |editor, _| {
 601            editor.set_nav_history(Some(nav_history));
 602        });
 603    }
 604
 605    fn navigate(
 606        &mut self,
 607        data: Box<dyn Any>,
 608        window: &mut Window,
 609        cx: &mut Context<Self>,
 610    ) -> bool {
 611        self.results_editor
 612            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 613    }
 614
 615    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
 616        match event {
 617            ViewEvent::UpdateTab => {
 618                f(ItemEvent::UpdateBreadcrumbs);
 619                f(ItemEvent::UpdateTab);
 620            }
 621            ViewEvent::EditorEvent(editor_event) => {
 622                Editor::to_item_events(editor_event, f);
 623            }
 624            ViewEvent::Dismiss => f(ItemEvent::CloseItem),
 625            _ => {}
 626        }
 627    }
 628
 629    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 630        if self.has_matches() {
 631            ToolbarItemLocation::Secondary
 632        } else {
 633            ToolbarItemLocation::Hidden
 634        }
 635    }
 636
 637    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
 638        self.results_editor.breadcrumbs(theme, cx)
 639    }
 640}
 641
 642impl ProjectSearchView {
 643    pub fn get_matches(&self, cx: &App) -> Vec<Range<Anchor>> {
 644        self.entity.read(cx).match_ranges.clone()
 645    }
 646
 647    fn toggle_filters(&mut self, cx: &mut Context<Self>) {
 648        self.filters_enabled = !self.filters_enabled;
 649        ActiveSettings::update_global(cx, |settings, cx| {
 650            settings.0.insert(
 651                self.entity.read(cx).project.downgrade(),
 652                self.current_settings(),
 653            );
 654        });
 655    }
 656
 657    fn current_settings(&self) -> ProjectSearchSettings {
 658        ProjectSearchSettings {
 659            search_options: self.search_options,
 660            filters_enabled: self.filters_enabled,
 661        }
 662    }
 663
 664    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context<Self>) {
 665        self.search_options.toggle(option);
 666        ActiveSettings::update_global(cx, |settings, cx| {
 667            settings.0.insert(
 668                self.entity.read(cx).project.downgrade(),
 669                self.current_settings(),
 670            );
 671        });
 672        self.adjust_query_regex_language(cx);
 673    }
 674
 675    fn toggle_opened_only(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
 676        self.included_opened_only = !self.included_opened_only;
 677    }
 678
 679    pub fn replacement(&self, cx: &App) -> String {
 680        self.replacement_editor.read(cx).text(cx)
 681    }
 682
 683    fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
 684        if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text
 685            && self.query_editor.read(cx).text(cx) != *last_search_query_text
 686        {
 687            // search query has changed, restart search and bail
 688            self.search(cx);
 689            return;
 690        }
 691        if self.entity.read(cx).match_ranges.is_empty() {
 692            return;
 693        }
 694        let Some(active_index) = self.active_match_index else {
 695            return;
 696        };
 697
 698        let query = self.entity.read(cx).active_query.clone();
 699        if let Some(query) = query {
 700            let query = query.with_replacement(self.replacement(cx));
 701
 702            // TODO: Do we need the clone here?
 703            let mat = self.entity.read(cx).match_ranges[active_index].clone();
 704            self.results_editor.update(cx, |editor, cx| {
 705                editor.replace(&mat, &query, window, cx);
 706            });
 707            self.select_match(Direction::Next, window, cx)
 708        }
 709    }
 710    fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
 711        if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text
 712            && self.query_editor.read(cx).text(cx) != *last_search_query_text
 713        {
 714            // search query has changed, restart search and bail
 715            self.search(cx);
 716            return;
 717        }
 718        if self.active_match_index.is_none() {
 719            return;
 720        }
 721        let Some(query) = self.entity.read(cx).active_query.as_ref() else {
 722            return;
 723        };
 724        let query = query.clone().with_replacement(self.replacement(cx));
 725
 726        let match_ranges = self
 727            .entity
 728            .update(cx, |model, _| mem::take(&mut model.match_ranges));
 729        if match_ranges.is_empty() {
 730            return;
 731        }
 732
 733        self.results_editor.update(cx, |editor, cx| {
 734            editor.replace_all(&mut match_ranges.iter(), &query, window, cx);
 735        });
 736
 737        self.entity.update(cx, |model, _cx| {
 738            model.match_ranges = match_ranges;
 739        });
 740    }
 741
 742    pub fn new(
 743        workspace: WeakEntity<Workspace>,
 744        entity: Entity<ProjectSearch>,
 745        window: &mut Window,
 746        cx: &mut Context<Self>,
 747        settings: Option<ProjectSearchSettings>,
 748    ) -> Self {
 749        let project;
 750        let excerpts;
 751        let mut replacement_text = None;
 752        let mut query_text = String::new();
 753        let mut subscriptions = Vec::new();
 754
 755        // Read in settings if available
 756        let (mut options, filters_enabled) = if let Some(settings) = settings {
 757            (settings.search_options, settings.filters_enabled)
 758        } else {
 759            let search_options =
 760                SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
 761            (search_options, false)
 762        };
 763
 764        {
 765            let entity = entity.read(cx);
 766            project = entity.project.clone();
 767            excerpts = entity.excerpts.clone();
 768            if let Some(active_query) = entity.active_query.as_ref() {
 769                query_text = active_query.as_str().to_string();
 770                replacement_text = active_query.replacement().map(ToOwned::to_owned);
 771                options = SearchOptions::from_query(active_query);
 772            }
 773        }
 774        subscriptions.push(cx.observe_in(&entity, window, |this, _, window, cx| {
 775            this.entity_changed(window, cx)
 776        }));
 777
 778        let query_editor = cx.new(|cx| {
 779            let mut editor = Editor::single_line(window, cx);
 780            editor.set_placeholder_text("Search all files…", window, cx);
 781            editor.set_text(query_text, window, cx);
 782            editor
 783        });
 784        // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
 785        subscriptions.push(
 786            cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| {
 787                if let EditorEvent::Edited { .. } = event
 788                    && EditorSettings::get_global(cx).use_smartcase_search
 789                {
 790                    let query = this.search_query_text(cx);
 791                    if !query.is_empty()
 792                        && this.search_options.contains(SearchOptions::CASE_SENSITIVE)
 793                            != contains_uppercase(&query)
 794                    {
 795                        this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
 796                    }
 797                }
 798                cx.emit(ViewEvent::EditorEvent(event.clone()))
 799            }),
 800        );
 801        let replacement_editor = cx.new(|cx| {
 802            let mut editor = Editor::single_line(window, cx);
 803            editor.set_placeholder_text("Replace in project…", window, cx);
 804            if let Some(text) = replacement_text {
 805                editor.set_text(text, window, cx);
 806            }
 807            editor
 808        });
 809        let results_editor = cx.new(|cx| {
 810            let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), window, cx);
 811            editor.set_searchable(false);
 812            editor.set_in_project_search(true);
 813            editor
 814        });
 815        subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)));
 816
 817        subscriptions.push(
 818            cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| {
 819                if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) {
 820                    this.update_match_index(cx);
 821                }
 822                // Reraise editor events for workspace item activation purposes
 823                cx.emit(ViewEvent::EditorEvent(event.clone()));
 824            }),
 825        );
 826
 827        let included_files_editor = cx.new(|cx| {
 828            let mut editor = Editor::single_line(window, cx);
 829            editor.set_placeholder_text("Include: crates/**/*.toml", window, cx);
 830
 831            editor
 832        });
 833        // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
 834        subscriptions.push(
 835            cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| {
 836                cx.emit(ViewEvent::EditorEvent(event.clone()))
 837            }),
 838        );
 839
 840        let excluded_files_editor = cx.new(|cx| {
 841            let mut editor = Editor::single_line(window, cx);
 842            editor.set_placeholder_text("Exclude: vendor/*, *.lock", window, cx);
 843
 844            editor
 845        });
 846        // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
 847        subscriptions.push(
 848            cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| {
 849                cx.emit(ViewEvent::EditorEvent(event.clone()))
 850            }),
 851        );
 852
 853        let focus_handle = cx.focus_handle();
 854        subscriptions.push(cx.on_focus(&focus_handle, window, |_, window, cx| {
 855            cx.on_next_frame(window, |this, window, cx| {
 856                if this.focus_handle.is_focused(window) {
 857                    if this.has_matches() {
 858                        this.results_editor.focus_handle(cx).focus(window);
 859                    } else {
 860                        this.query_editor.focus_handle(cx).focus(window);
 861                    }
 862                }
 863            });
 864        }));
 865
 866        let languages = project.read(cx).languages().clone();
 867        cx.spawn(async move |project_search_view, cx| {
 868            let regex_language = languages
 869                .language_for_name("regex")
 870                .await
 871                .context("loading regex language")?;
 872            project_search_view
 873                .update(cx, |project_search_view, cx| {
 874                    project_search_view.regex_language = Some(regex_language);
 875                    project_search_view.adjust_query_regex_language(cx);
 876                })
 877                .ok();
 878            anyhow::Ok(())
 879        })
 880        .detach_and_log_err(cx);
 881
 882        // Check if Worktrees have all been previously indexed
 883        let mut this = ProjectSearchView {
 884            workspace,
 885            focus_handle,
 886            replacement_editor,
 887            search_id: entity.read(cx).search_id,
 888            entity,
 889            query_editor,
 890            results_editor,
 891            search_options: options,
 892            panels_with_errors: HashMap::default(),
 893            active_match_index: None,
 894            included_files_editor,
 895            excluded_files_editor,
 896            filters_enabled,
 897            replace_enabled: false,
 898            included_opened_only: false,
 899            regex_language: None,
 900            _subscriptions: subscriptions,
 901        };
 902        this.entity_changed(window, cx);
 903        this
 904    }
 905
 906    pub fn new_search_in_directory(
 907        workspace: &mut Workspace,
 908        dir_path: &RelPath,
 909        window: &mut Window,
 910        cx: &mut Context<Workspace>,
 911    ) {
 912        let filter_str = dir_path.display(workspace.path_style(cx));
 913
 914        let weak_workspace = cx.entity().downgrade();
 915
 916        let entity = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx));
 917        let search = cx.new(|cx| ProjectSearchView::new(weak_workspace, entity, window, cx, None));
 918        workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, window, cx);
 919        search.update(cx, |search, cx| {
 920            search
 921                .included_files_editor
 922                .update(cx, |editor, cx| editor.set_text(filter_str, window, cx));
 923            search.filters_enabled = true;
 924            search.focus_query_editor(window, cx)
 925        });
 926    }
 927
 928    /// Re-activate the most recently activated search in this pane or the most recent if it has been closed.
 929    /// If no search exists in the workspace, create a new one.
 930    pub fn deploy_search(
 931        workspace: &mut Workspace,
 932        action: &workspace::DeploySearch,
 933        window: &mut Window,
 934        cx: &mut Context<Workspace>,
 935    ) {
 936        let existing = workspace
 937            .active_pane()
 938            .read(cx)
 939            .items()
 940            .find_map(|item| item.downcast::<ProjectSearchView>());
 941
 942        Self::existing_or_new_search(workspace, existing, action, window, cx);
 943    }
 944
 945    fn search_in_new(
 946        workspace: &mut Workspace,
 947        _: &SearchInNew,
 948        window: &mut Window,
 949        cx: &mut Context<Workspace>,
 950    ) {
 951        if let Some(search_view) = workspace
 952            .active_item(cx)
 953            .and_then(|item| item.downcast::<ProjectSearchView>())
 954        {
 955            let new_query = search_view.update(cx, |search_view, cx| {
 956                let open_buffers = if search_view.included_opened_only {
 957                    Some(search_view.open_buffers(cx, workspace))
 958                } else {
 959                    None
 960                };
 961                let new_query = search_view.build_search_query(cx, open_buffers);
 962                if new_query.is_some()
 963                    && let Some(old_query) = search_view.entity.read(cx).active_query.clone()
 964                {
 965                    search_view.query_editor.update(cx, |editor, cx| {
 966                        editor.set_text(old_query.as_str(), window, cx);
 967                    });
 968                    search_view.search_options = SearchOptions::from_query(&old_query);
 969                    search_view.adjust_query_regex_language(cx);
 970                }
 971                new_query
 972            });
 973            if let Some(new_query) = new_query {
 974                let entity = cx.new(|cx| {
 975                    let mut entity = ProjectSearch::new(workspace.project().clone(), cx);
 976                    entity.search(new_query, cx);
 977                    entity
 978                });
 979                let weak_workspace = cx.entity().downgrade();
 980                workspace.add_item_to_active_pane(
 981                    Box::new(cx.new(|cx| {
 982                        ProjectSearchView::new(weak_workspace, entity, window, cx, None)
 983                    })),
 984                    None,
 985                    true,
 986                    window,
 987                    cx,
 988                );
 989            }
 990        }
 991    }
 992
 993    // Add another search tab to the workspace.
 994    fn new_search(
 995        workspace: &mut Workspace,
 996        _: &workspace::NewSearch,
 997        window: &mut Window,
 998        cx: &mut Context<Workspace>,
 999    ) {
1000        Self::existing_or_new_search(workspace, None, &DeploySearch::find(), window, cx)
1001    }
1002
1003    fn existing_or_new_search(
1004        workspace: &mut Workspace,
1005        existing: Option<Entity<ProjectSearchView>>,
1006        action: &workspace::DeploySearch,
1007        window: &mut Window,
1008        cx: &mut Context<Workspace>,
1009    ) {
1010        let query = workspace.active_item(cx).and_then(|item| {
1011            if let Some(buffer_search_query) = buffer_search_query(workspace, item.as_ref(), cx) {
1012                return Some(buffer_search_query);
1013            }
1014
1015            let editor = item.act_as::<Editor>(cx)?;
1016            let query = editor.query_suggestion(window, cx);
1017            if query.is_empty() { None } else { Some(query) }
1018        });
1019
1020        let search = if let Some(existing) = existing {
1021            workspace.activate_item(&existing, true, true, window, cx);
1022            existing
1023        } else {
1024            let settings = cx
1025                .global::<ActiveSettings>()
1026                .0
1027                .get(&workspace.project().downgrade());
1028
1029            let settings = settings.cloned();
1030
1031            let weak_workspace = cx.entity().downgrade();
1032
1033            let project_search = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx));
1034            let project_search_view = cx.new(|cx| {
1035                ProjectSearchView::new(weak_workspace, project_search, window, cx, settings)
1036            });
1037
1038            workspace.add_item_to_active_pane(
1039                Box::new(project_search_view.clone()),
1040                None,
1041                true,
1042                window,
1043                cx,
1044            );
1045            project_search_view
1046        };
1047
1048        search.update(cx, |search, cx| {
1049            search.replace_enabled = action.replace_enabled;
1050            if let Some(query) = query {
1051                search.set_query(&query, window, cx);
1052            }
1053            if let Some(included_files) = action.included_files.as_deref() {
1054                search
1055                    .included_files_editor
1056                    .update(cx, |editor, cx| editor.set_text(included_files, window, cx));
1057                search.filters_enabled = true;
1058            }
1059            if let Some(excluded_files) = action.excluded_files.as_deref() {
1060                search
1061                    .excluded_files_editor
1062                    .update(cx, |editor, cx| editor.set_text(excluded_files, window, cx));
1063                search.filters_enabled = true;
1064            }
1065            search.focus_query_editor(window, cx)
1066        });
1067    }
1068
1069    fn prompt_to_save_if_dirty_then_search(
1070        &mut self,
1071        window: &mut Window,
1072        cx: &mut Context<Self>,
1073    ) -> Task<anyhow::Result<()>> {
1074        let project = self.entity.read(cx).project.clone();
1075
1076        let can_autosave = self.results_editor.can_autosave(cx);
1077        let autosave_setting = self.results_editor.workspace_settings(cx).autosave;
1078
1079        let will_autosave = can_autosave && autosave_setting.should_save_on_close();
1080
1081        let is_dirty = self.is_dirty(cx);
1082
1083        cx.spawn_in(window, async move |this, cx| {
1084            let skip_save_on_close = this
1085                .read_with(cx, |this, cx| {
1086                    this.workspace.read_with(cx, |workspace, cx| {
1087                        workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx)
1088                    })
1089                })?
1090                .unwrap_or(false);
1091
1092            let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
1093
1094            let should_search = if should_prompt_to_save {
1095                let options = &["Save", "Don't Save", "Cancel"];
1096                let result_channel = this.update_in(cx, |_, window, cx| {
1097                    window.prompt(
1098                        gpui::PromptLevel::Warning,
1099                        "Project search buffer contains unsaved edits. Do you want to save it?",
1100                        None,
1101                        options,
1102                        cx,
1103                    )
1104                })?;
1105                let result = result_channel.await?;
1106                let should_save = result == 0;
1107                if should_save {
1108                    this.update_in(cx, |this, window, cx| {
1109                        this.save(
1110                            SaveOptions {
1111                                format: true,
1112                                autosave: false,
1113                            },
1114                            project,
1115                            window,
1116                            cx,
1117                        )
1118                    })?
1119                    .await
1120                    .log_err();
1121                }
1122
1123                result != 2
1124            } else {
1125                true
1126            };
1127            if should_search {
1128                this.update(cx, |this, cx| {
1129                    this.search(cx);
1130                })?;
1131            }
1132            anyhow::Ok(())
1133        })
1134    }
1135
1136    fn search(&mut self, cx: &mut Context<Self>) {
1137        let open_buffers = if self.included_opened_only {
1138            self.workspace
1139                .update(cx, |workspace, cx| self.open_buffers(cx, workspace))
1140                .ok()
1141        } else {
1142            None
1143        };
1144        if let Some(query) = self.build_search_query(cx, open_buffers) {
1145            self.entity.update(cx, |model, cx| model.search(query, cx));
1146        }
1147    }
1148
1149    pub fn search_query_text(&self, cx: &App) -> String {
1150        self.query_editor.read(cx).text(cx)
1151    }
1152
1153    fn build_search_query(
1154        &mut self,
1155        cx: &mut Context<Self>,
1156        open_buffers: Option<Vec<Entity<Buffer>>>,
1157    ) -> Option<SearchQuery> {
1158        // Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
1159
1160        let text = self.search_query_text(cx);
1161        let included_files = self
1162            .filters_enabled
1163            .then(|| {
1164                match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) {
1165                    Ok(included_files) => {
1166                        let should_unmark_error =
1167                            self.panels_with_errors.remove(&InputPanel::Include);
1168                        if should_unmark_error.is_some() {
1169                            cx.notify();
1170                        }
1171                        included_files
1172                    }
1173                    Err(e) => {
1174                        let should_mark_error = self
1175                            .panels_with_errors
1176                            .insert(InputPanel::Include, e.to_string());
1177                        if should_mark_error.is_none() {
1178                            cx.notify();
1179                        }
1180                        PathMatcher::default()
1181                    }
1182                }
1183            })
1184            .unwrap_or(PathMatcher::default());
1185        let excluded_files = self
1186            .filters_enabled
1187            .then(|| {
1188                match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) {
1189                    Ok(excluded_files) => {
1190                        let should_unmark_error =
1191                            self.panels_with_errors.remove(&InputPanel::Exclude);
1192                        if should_unmark_error.is_some() {
1193                            cx.notify();
1194                        }
1195
1196                        excluded_files
1197                    }
1198                    Err(e) => {
1199                        let should_mark_error = self
1200                            .panels_with_errors
1201                            .insert(InputPanel::Exclude, e.to_string());
1202                        if should_mark_error.is_none() {
1203                            cx.notify();
1204                        }
1205                        PathMatcher::default()
1206                    }
1207                }
1208            })
1209            .unwrap_or(PathMatcher::default());
1210
1211        // If the project contains multiple visible worktrees, we match the
1212        // include/exclude patterns against full paths to allow them to be
1213        // disambiguated. For single worktree projects we use worktree relative
1214        // paths for convenience.
1215        let match_full_paths = self
1216            .entity
1217            .read(cx)
1218            .project
1219            .read(cx)
1220            .visible_worktrees(cx)
1221            .count()
1222            > 1;
1223
1224        let query = if self.search_options.contains(SearchOptions::REGEX) {
1225            match SearchQuery::regex(
1226                text,
1227                self.search_options.contains(SearchOptions::WHOLE_WORD),
1228                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1229                self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1230                self.search_options
1231                    .contains(SearchOptions::ONE_MATCH_PER_LINE),
1232                included_files,
1233                excluded_files,
1234                match_full_paths,
1235                open_buffers,
1236            ) {
1237                Ok(query) => {
1238                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
1239                    if should_unmark_error.is_some() {
1240                        cx.notify();
1241                    }
1242
1243                    Some(query)
1244                }
1245                Err(e) => {
1246                    let should_mark_error = self
1247                        .panels_with_errors
1248                        .insert(InputPanel::Query, e.to_string());
1249                    if should_mark_error.is_none() {
1250                        cx.notify();
1251                    }
1252
1253                    None
1254                }
1255            }
1256        } else {
1257            match SearchQuery::text(
1258                text,
1259                self.search_options.contains(SearchOptions::WHOLE_WORD),
1260                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
1261                self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
1262                included_files,
1263                excluded_files,
1264                match_full_paths,
1265                open_buffers,
1266            ) {
1267                Ok(query) => {
1268                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
1269                    if should_unmark_error.is_some() {
1270                        cx.notify();
1271                    }
1272
1273                    Some(query)
1274                }
1275                Err(e) => {
1276                    let should_mark_error = self
1277                        .panels_with_errors
1278                        .insert(InputPanel::Query, e.to_string());
1279                    if should_mark_error.is_none() {
1280                        cx.notify();
1281                    }
1282
1283                    None
1284                }
1285            }
1286        };
1287        if !self.panels_with_errors.is_empty() {
1288            return None;
1289        }
1290        if query.as_ref().is_some_and(|query| query.is_empty()) {
1291            return None;
1292        }
1293        query
1294    }
1295
1296    fn open_buffers(&self, cx: &App, workspace: &Workspace) -> Vec<Entity<Buffer>> {
1297        let mut buffers = Vec::new();
1298        for editor in workspace.items_of_type::<Editor>(cx) {
1299            if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
1300                buffers.push(buffer);
1301            }
1302        }
1303        buffers
1304    }
1305
1306    fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result<PathMatcher> {
1307        let path_style = self.entity.read(cx).project.read(cx).path_style(cx);
1308        let queries = text
1309            .split(',')
1310            .map(str::trim)
1311            .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
1312            .map(str::to_owned)
1313            .collect::<Vec<_>>();
1314        Ok(PathMatcher::new(&queries, path_style)?)
1315    }
1316
1317    fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1318        if let Some(index) = self.active_match_index {
1319            let match_ranges = self.entity.read(cx).match_ranges.clone();
1320
1321            if !EditorSettings::get_global(cx).search_wrap
1322                && ((direction == Direction::Next && index + 1 >= match_ranges.len())
1323                    || (direction == Direction::Prev && index == 0))
1324            {
1325                crate::show_no_more_matches(window, cx);
1326                return;
1327            }
1328
1329            let new_index = self.results_editor.update(cx, |editor, cx| {
1330                editor.match_index_for_direction(&match_ranges, index, direction, 1, window, cx)
1331            });
1332
1333            let range_to_select = match_ranges[new_index].clone();
1334            self.results_editor.update(cx, |editor, cx| {
1335                let range_to_select = editor.range_for_match(&range_to_select);
1336                editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx);
1337                editor.change_selections(Default::default(), window, cx, |s| {
1338                    s.select_ranges([range_to_select])
1339                });
1340            });
1341        }
1342    }
1343
1344    fn focus_query_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1345        self.query_editor.update(cx, |query_editor, cx| {
1346            query_editor.select_all(&SelectAll, window, cx);
1347        });
1348        let editor_handle = self.query_editor.focus_handle(cx);
1349        window.focus(&editor_handle);
1350    }
1351
1352    fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
1353        self.set_search_editor(SearchInputKind::Query, query, window, cx);
1354        if EditorSettings::get_global(cx).use_smartcase_search
1355            && !query.is_empty()
1356            && self.search_options.contains(SearchOptions::CASE_SENSITIVE)
1357                != contains_uppercase(query)
1358        {
1359            self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
1360        }
1361    }
1362
1363    fn set_search_editor(
1364        &mut self,
1365        kind: SearchInputKind,
1366        text: &str,
1367        window: &mut Window,
1368        cx: &mut Context<Self>,
1369    ) {
1370        let editor = match kind {
1371            SearchInputKind::Query => &self.query_editor,
1372            SearchInputKind::Include => &self.included_files_editor,
1373
1374            SearchInputKind::Exclude => &self.excluded_files_editor,
1375        };
1376        editor.update(cx, |included_editor, cx| {
1377            included_editor.set_text(text, window, cx)
1378        });
1379    }
1380
1381    fn focus_results_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1382        self.query_editor.update(cx, |query_editor, cx| {
1383            let cursor = query_editor.selections.newest_anchor().head();
1384            query_editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1385                s.select_ranges([cursor..cursor])
1386            });
1387        });
1388        let results_handle = self.results_editor.focus_handle(cx);
1389        window.focus(&results_handle);
1390    }
1391
1392    fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1393        let match_ranges = self.entity.read(cx).match_ranges.clone();
1394        if match_ranges.is_empty() {
1395            self.active_match_index = None;
1396            self.results_editor.update(cx, |editor, cx| {
1397                editor.clear_background_highlights::<Self>(cx);
1398            });
1399        } else {
1400            self.active_match_index = Some(0);
1401            self.update_match_index(cx);
1402            let prev_search_id = mem::replace(&mut self.search_id, self.entity.read(cx).search_id);
1403            let is_new_search = self.search_id != prev_search_id;
1404            self.results_editor.update(cx, |editor, cx| {
1405                if is_new_search {
1406                    let range_to_select = match_ranges
1407                        .first()
1408                        .map(|range| editor.range_for_match(range));
1409                    editor.change_selections(Default::default(), window, cx, |s| {
1410                        s.select_ranges(range_to_select)
1411                    });
1412                    editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
1413                }
1414                editor.highlight_background::<Self>(
1415                    &match_ranges,
1416                    |theme| theme.colors().search_match_background,
1417                    cx,
1418                );
1419            });
1420            if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) {
1421                self.focus_results_editor(window, cx);
1422            }
1423        }
1424
1425        cx.emit(ViewEvent::UpdateTab);
1426        cx.notify();
1427    }
1428
1429    fn update_match_index(&mut self, cx: &mut Context<Self>) {
1430        let results_editor = self.results_editor.read(cx);
1431        let new_index = active_match_index(
1432            Direction::Next,
1433            &self.entity.read(cx).match_ranges,
1434            &results_editor.selections.newest_anchor().head(),
1435            &results_editor.buffer().read(cx).snapshot(cx),
1436        );
1437        if self.active_match_index != new_index {
1438            self.active_match_index = new_index;
1439            cx.notify();
1440        }
1441    }
1442
1443    pub fn has_matches(&self) -> bool {
1444        self.active_match_index.is_some()
1445    }
1446
1447    fn landing_text_minor(&self, window: &mut Window, cx: &App) -> impl IntoElement {
1448        let focus_handle = self.focus_handle.clone();
1449        v_flex()
1450            .gap_1()
1451            .child(
1452                Label::new("Hit enter to search. For more options:")
1453                    .color(Color::Muted)
1454                    .mb_2(),
1455            )
1456            .child(
1457                Button::new("filter-paths", "Include/exclude specific paths")
1458                    .icon(IconName::Filter)
1459                    .icon_position(IconPosition::Start)
1460                    .icon_size(IconSize::Small)
1461                    .key_binding(KeyBinding::for_action_in(
1462                        &ToggleFilters,
1463                        &focus_handle,
1464                        window,
1465                        cx,
1466                    ))
1467                    .on_click(|_event, window, cx| {
1468                        window.dispatch_action(ToggleFilters.boxed_clone(), cx)
1469                    }),
1470            )
1471            .child(
1472                Button::new("find-replace", "Find and replace")
1473                    .icon(IconName::Replace)
1474                    .icon_position(IconPosition::Start)
1475                    .icon_size(IconSize::Small)
1476                    .key_binding(KeyBinding::for_action_in(
1477                        &ToggleReplace,
1478                        &focus_handle,
1479                        window,
1480                        cx,
1481                    ))
1482                    .on_click(|_event, window, cx| {
1483                        window.dispatch_action(ToggleReplace.boxed_clone(), cx)
1484                    }),
1485            )
1486            .child(
1487                Button::new("regex", "Match with regex")
1488                    .icon(IconName::Regex)
1489                    .icon_position(IconPosition::Start)
1490                    .icon_size(IconSize::Small)
1491                    .key_binding(KeyBinding::for_action_in(
1492                        &ToggleRegex,
1493                        &focus_handle,
1494                        window,
1495                        cx,
1496                    ))
1497                    .on_click(|_event, window, cx| {
1498                        window.dispatch_action(ToggleRegex.boxed_clone(), cx)
1499                    }),
1500            )
1501            .child(
1502                Button::new("match-case", "Match case")
1503                    .icon(IconName::CaseSensitive)
1504                    .icon_position(IconPosition::Start)
1505                    .icon_size(IconSize::Small)
1506                    .key_binding(KeyBinding::for_action_in(
1507                        &ToggleCaseSensitive,
1508                        &focus_handle,
1509                        window,
1510                        cx,
1511                    ))
1512                    .on_click(|_event, window, cx| {
1513                        window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx)
1514                    }),
1515            )
1516            .child(
1517                Button::new("match-whole-words", "Match whole words")
1518                    .icon(IconName::WholeWord)
1519                    .icon_position(IconPosition::Start)
1520                    .icon_size(IconSize::Small)
1521                    .key_binding(KeyBinding::for_action_in(
1522                        &ToggleWholeWord,
1523                        &focus_handle,
1524                        window,
1525                        cx,
1526                    ))
1527                    .on_click(|_event, window, cx| {
1528                        window.dispatch_action(ToggleWholeWord.boxed_clone(), cx)
1529                    }),
1530            )
1531    }
1532
1533    fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla {
1534        if self.panels_with_errors.contains_key(&panel) {
1535            Color::Error.color(cx)
1536        } else {
1537            cx.theme().colors().border
1538        }
1539    }
1540
1541    fn move_focus_to_results(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1542        if !self.results_editor.focus_handle(cx).is_focused(window)
1543            && !self.entity.read(cx).match_ranges.is_empty()
1544        {
1545            cx.stop_propagation();
1546            self.focus_results_editor(window, cx)
1547        }
1548    }
1549
1550    #[cfg(any(test, feature = "test-support"))]
1551    pub fn results_editor(&self) -> &Entity<Editor> {
1552        &self.results_editor
1553    }
1554
1555    fn adjust_query_regex_language(&self, cx: &mut App) {
1556        let enable = self.search_options.contains(SearchOptions::REGEX);
1557        let query_buffer = self
1558            .query_editor
1559            .read(cx)
1560            .buffer()
1561            .read(cx)
1562            .as_singleton()
1563            .expect("query editor should be backed by a singleton buffer");
1564        if enable {
1565            if let Some(regex_language) = self.regex_language.clone() {
1566                query_buffer.update(cx, |query_buffer, cx| {
1567                    query_buffer.set_language(Some(regex_language), cx);
1568                })
1569            }
1570        } else {
1571            query_buffer.update(cx, |query_buffer, cx| {
1572                query_buffer.set_language(None, cx);
1573            })
1574        }
1575    }
1576}
1577
1578fn buffer_search_query(
1579    workspace: &mut Workspace,
1580    item: &dyn ItemHandle,
1581    cx: &mut Context<Workspace>,
1582) -> Option<String> {
1583    let buffer_search_bar = workspace
1584        .pane_for(item)
1585        .and_then(|pane| {
1586            pane.read(cx)
1587                .toolbar()
1588                .read(cx)
1589                .item_of_type::<BufferSearchBar>()
1590        })?
1591        .read(cx);
1592    if buffer_search_bar.query_editor_focused() {
1593        let buffer_search_query = buffer_search_bar.query(cx);
1594        if !buffer_search_query.is_empty() {
1595            return Some(buffer_search_query);
1596        }
1597    }
1598    None
1599}
1600
1601impl Default for ProjectSearchBar {
1602    fn default() -> Self {
1603        Self::new()
1604    }
1605}
1606
1607impl ProjectSearchBar {
1608    pub fn new() -> Self {
1609        Self {
1610            active_project_search: None,
1611            subscription: None,
1612        }
1613    }
1614
1615    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1616        if let Some(search_view) = self.active_project_search.as_ref() {
1617            search_view.update(cx, |search_view, cx| {
1618                if !search_view
1619                    .replacement_editor
1620                    .focus_handle(cx)
1621                    .is_focused(window)
1622                {
1623                    cx.stop_propagation();
1624                    search_view
1625                        .prompt_to_save_if_dirty_then_search(window, cx)
1626                        .detach_and_log_err(cx);
1627                }
1628            });
1629        }
1630    }
1631
1632    fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
1633        self.cycle_field(Direction::Next, window, cx);
1634    }
1635
1636    fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
1637        self.cycle_field(Direction::Prev, window, cx);
1638    }
1639
1640    fn focus_search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1641        if let Some(search_view) = self.active_project_search.as_ref() {
1642            search_view.update(cx, |search_view, cx| {
1643                search_view.query_editor.focus_handle(cx).focus(window);
1644            });
1645        }
1646    }
1647
1648    fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
1649        let active_project_search = match &self.active_project_search {
1650            Some(active_project_search) => active_project_search,
1651            None => return,
1652        };
1653
1654        active_project_search.update(cx, |project_view, cx| {
1655            let mut views = vec![project_view.query_editor.focus_handle(cx)];
1656            if project_view.replace_enabled {
1657                views.push(project_view.replacement_editor.focus_handle(cx));
1658            }
1659            if project_view.filters_enabled {
1660                views.extend([
1661                    project_view.included_files_editor.focus_handle(cx),
1662                    project_view.excluded_files_editor.focus_handle(cx),
1663                ]);
1664            }
1665            let current_index = match views.iter().position(|focus| focus.is_focused(window)) {
1666                Some(index) => index,
1667                None => return,
1668            };
1669
1670            let new_index = match direction {
1671                Direction::Next => (current_index + 1) % views.len(),
1672                Direction::Prev if current_index == 0 => views.len() - 1,
1673                Direction::Prev => (current_index - 1) % views.len(),
1674            };
1675            let next_focus_handle = &views[new_index];
1676            window.focus(next_focus_handle);
1677            cx.stop_propagation();
1678        });
1679    }
1680
1681    pub(crate) fn toggle_search_option(
1682        &mut self,
1683        option: SearchOptions,
1684        window: &mut Window,
1685        cx: &mut Context<Self>,
1686    ) -> bool {
1687        if self.active_project_search.is_none() {
1688            return false;
1689        }
1690
1691        cx.spawn_in(window, async move |this, cx| {
1692            let task = this.update_in(cx, |this, window, cx| {
1693                let search_view = this.active_project_search.as_ref()?;
1694                search_view.update(cx, |search_view, cx| {
1695                    search_view.toggle_search_option(option, cx);
1696                    search_view
1697                        .entity
1698                        .read(cx)
1699                        .active_query
1700                        .is_some()
1701                        .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
1702                })
1703            })?;
1704            if let Some(task) = task {
1705                task.await?;
1706            }
1707            this.update(cx, |_, cx| {
1708                cx.notify();
1709            })?;
1710            anyhow::Ok(())
1711        })
1712        .detach();
1713        true
1714    }
1715
1716    fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
1717        if let Some(search) = &self.active_project_search {
1718            search.update(cx, |this, cx| {
1719                this.replace_enabled = !this.replace_enabled;
1720                let editor_to_focus = if this.replace_enabled {
1721                    this.replacement_editor.focus_handle(cx)
1722                } else {
1723                    this.query_editor.focus_handle(cx)
1724                };
1725                window.focus(&editor_to_focus);
1726                cx.notify();
1727            });
1728        }
1729    }
1730
1731    fn toggle_filters(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1732        if let Some(search_view) = self.active_project_search.as_ref() {
1733            search_view.update(cx, |search_view, cx| {
1734                search_view.toggle_filters(cx);
1735                search_view
1736                    .included_files_editor
1737                    .update(cx, |_, cx| cx.notify());
1738                search_view
1739                    .excluded_files_editor
1740                    .update(cx, |_, cx| cx.notify());
1741                window.refresh();
1742                cx.notify();
1743            });
1744            cx.notify();
1745            true
1746        } else {
1747            false
1748        }
1749    }
1750
1751    fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1752        if self.active_project_search.is_none() {
1753            return false;
1754        }
1755
1756        cx.spawn_in(window, async move |this, cx| {
1757            let task = this.update_in(cx, |this, window, cx| {
1758                let search_view = this.active_project_search.as_ref()?;
1759                search_view.update(cx, |search_view, cx| {
1760                    search_view.toggle_opened_only(window, cx);
1761                    search_view
1762                        .entity
1763                        .read(cx)
1764                        .active_query
1765                        .is_some()
1766                        .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
1767                })
1768            })?;
1769            if let Some(task) = task {
1770                task.await?;
1771            }
1772            this.update(cx, |_, cx| {
1773                cx.notify();
1774            })?;
1775            anyhow::Ok(())
1776        })
1777        .detach();
1778        true
1779    }
1780
1781    fn is_opened_only_enabled(&self, cx: &App) -> bool {
1782        if let Some(search_view) = self.active_project_search.as_ref() {
1783            search_view.read(cx).included_opened_only
1784        } else {
1785            false
1786        }
1787    }
1788
1789    fn move_focus_to_results(&self, window: &mut Window, cx: &mut Context<Self>) {
1790        if let Some(search_view) = self.active_project_search.as_ref() {
1791            search_view.update(cx, |search_view, cx| {
1792                search_view.move_focus_to_results(window, cx);
1793            });
1794            cx.notify();
1795        }
1796    }
1797
1798    fn next_history_query(
1799        &mut self,
1800        _: &NextHistoryQuery,
1801        window: &mut Window,
1802        cx: &mut Context<Self>,
1803    ) {
1804        if let Some(search_view) = self.active_project_search.as_ref() {
1805            search_view.update(cx, |search_view, cx| {
1806                for (editor, kind) in [
1807                    (search_view.query_editor.clone(), SearchInputKind::Query),
1808                    (
1809                        search_view.included_files_editor.clone(),
1810                        SearchInputKind::Include,
1811                    ),
1812                    (
1813                        search_view.excluded_files_editor.clone(),
1814                        SearchInputKind::Exclude,
1815                    ),
1816                ] {
1817                    if editor.focus_handle(cx).is_focused(window) {
1818                        let new_query = search_view.entity.update(cx, |model, cx| {
1819                            let project = model.project.clone();
1820
1821                            if let Some(new_query) = project.update(cx, |project, _| {
1822                                project
1823                                    .search_history_mut(kind)
1824                                    .next(model.cursor_mut(kind))
1825                                    .map(str::to_string)
1826                            }) {
1827                                new_query
1828                            } else {
1829                                model.cursor_mut(kind).reset();
1830                                String::new()
1831                            }
1832                        });
1833                        search_view.set_search_editor(kind, &new_query, window, cx);
1834                    }
1835                }
1836            });
1837        }
1838    }
1839
1840    fn previous_history_query(
1841        &mut self,
1842        _: &PreviousHistoryQuery,
1843        window: &mut Window,
1844        cx: &mut Context<Self>,
1845    ) {
1846        if let Some(search_view) = self.active_project_search.as_ref() {
1847            search_view.update(cx, |search_view, cx| {
1848                for (editor, kind) in [
1849                    (search_view.query_editor.clone(), SearchInputKind::Query),
1850                    (
1851                        search_view.included_files_editor.clone(),
1852                        SearchInputKind::Include,
1853                    ),
1854                    (
1855                        search_view.excluded_files_editor.clone(),
1856                        SearchInputKind::Exclude,
1857                    ),
1858                ] {
1859                    if editor.focus_handle(cx).is_focused(window) {
1860                        if editor.read(cx).text(cx).is_empty()
1861                            && let Some(new_query) = search_view
1862                                .entity
1863                                .read(cx)
1864                                .project
1865                                .read(cx)
1866                                .search_history(kind)
1867                                .current(search_view.entity.read(cx).cursor(kind))
1868                                .map(str::to_string)
1869                        {
1870                            search_view.set_search_editor(kind, &new_query, window, cx);
1871                            return;
1872                        }
1873
1874                        if let Some(new_query) = search_view.entity.update(cx, |model, cx| {
1875                            let project = model.project.clone();
1876                            project.update(cx, |project, _| {
1877                                project
1878                                    .search_history_mut(kind)
1879                                    .previous(model.cursor_mut(kind))
1880                                    .map(str::to_string)
1881                            })
1882                        }) {
1883                            search_view.set_search_editor(kind, &new_query, window, cx);
1884                        }
1885                    }
1886                }
1887            });
1888        }
1889    }
1890
1891    fn select_next_match(
1892        &mut self,
1893        _: &SelectNextMatch,
1894        window: &mut Window,
1895        cx: &mut Context<Self>,
1896    ) {
1897        if let Some(search) = self.active_project_search.as_ref() {
1898            search.update(cx, |this, cx| {
1899                this.select_match(Direction::Next, window, cx);
1900            })
1901        }
1902    }
1903
1904    fn select_prev_match(
1905        &mut self,
1906        _: &SelectPreviousMatch,
1907        window: &mut Window,
1908        cx: &mut Context<Self>,
1909    ) {
1910        if let Some(search) = self.active_project_search.as_ref() {
1911            search.update(cx, |this, cx| {
1912                this.select_match(Direction::Prev, window, cx);
1913            })
1914        }
1915    }
1916}
1917
1918impl Render for ProjectSearchBar {
1919    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1920        let Some(search) = self.active_project_search.clone() else {
1921            return div();
1922        };
1923        let search = search.read(cx);
1924        let focus_handle = search.focus_handle(cx);
1925
1926        let container_width = window.viewport_size().width;
1927        let input_width = SearchInputWidth::calc_width(container_width);
1928
1929        let input_base_styles = |panel: InputPanel| {
1930            input_base_styles(search.border_color_for(panel, cx), |div| match panel {
1931                InputPanel::Query | InputPanel::Replacement => div.w(input_width),
1932                InputPanel::Include | InputPanel::Exclude => div.flex_grow(),
1933            })
1934        };
1935        let theme_colors = cx.theme().colors();
1936        let project_search = search.entity.read(cx);
1937        let limit_reached = project_search.limit_reached;
1938
1939        let color_override = match (
1940            &project_search.pending_search,
1941            project_search.no_results,
1942            &project_search.active_query,
1943            &project_search.last_search_query_text,
1944        ) {
1945            (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error),
1946            _ => None,
1947        };
1948
1949        let match_text = search
1950            .active_match_index
1951            .and_then(|index| {
1952                let index = index + 1;
1953                let match_quantity = project_search.match_ranges.len();
1954                if match_quantity > 0 {
1955                    debug_assert!(match_quantity >= index);
1956                    if limit_reached {
1957                        Some(format!("{index}/{match_quantity}+"))
1958                    } else {
1959                        Some(format!("{index}/{match_quantity}"))
1960                    }
1961                } else {
1962                    None
1963                }
1964            })
1965            .unwrap_or_else(|| "0/0".to_string());
1966
1967        let query_column = input_base_styles(InputPanel::Query)
1968            .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
1969            .on_action(cx.listener(|this, action, window, cx| {
1970                this.previous_history_query(action, window, cx)
1971            }))
1972            .on_action(
1973                cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)),
1974            )
1975            .child(render_text_input(&search.query_editor, color_override, cx))
1976            .child(
1977                h_flex()
1978                    .gap_1()
1979                    .child(SearchOption::CaseSensitive.as_button(
1980                        search.search_options,
1981                        SearchSource::Project(cx),
1982                        focus_handle.clone(),
1983                    ))
1984                    .child(SearchOption::WholeWord.as_button(
1985                        search.search_options,
1986                        SearchSource::Project(cx),
1987                        focus_handle.clone(),
1988                    ))
1989                    .child(SearchOption::Regex.as_button(
1990                        search.search_options,
1991                        SearchSource::Project(cx),
1992                        focus_handle.clone(),
1993                    )),
1994            );
1995
1996        let query_focus = search.query_editor.focus_handle(cx);
1997
1998        let matches_column = h_flex()
1999            .pl_2()
2000            .ml_2()
2001            .border_l_1()
2002            .border_color(theme_colors.border_variant)
2003            .child(render_action_button(
2004                "project-search-nav-button",
2005                IconName::ChevronLeft,
2006                search
2007                    .active_match_index
2008                    .is_none()
2009                    .then_some(ActionButtonState::Disabled),
2010                "Select Previous Match",
2011                &SelectPreviousMatch,
2012                query_focus.clone(),
2013            ))
2014            .child(render_action_button(
2015                "project-search-nav-button",
2016                IconName::ChevronRight,
2017                search
2018                    .active_match_index
2019                    .is_none()
2020                    .then_some(ActionButtonState::Disabled),
2021                "Select Next Match",
2022                &SelectNextMatch,
2023                query_focus,
2024            ))
2025            .child(
2026                div()
2027                    .id("matches")
2028                    .ml_2()
2029                    .min_w(rems_from_px(40.))
2030                    .child(Label::new(match_text).size(LabelSize::Small).color(
2031                        if search.active_match_index.is_some() {
2032                            Color::Default
2033                        } else {
2034                            Color::Disabled
2035                        },
2036                    ))
2037                    .when(limit_reached, |el| {
2038                        el.tooltip(Tooltip::text(
2039                            "Search limits reached.\nTry narrowing your search.",
2040                        ))
2041                    }),
2042            );
2043
2044        let mode_column = h_flex()
2045            .gap_1()
2046            .min_w_64()
2047            .child(
2048                IconButton::new("project-search-filter-button", IconName::Filter)
2049                    .shape(IconButtonShape::Square)
2050                    .tooltip(|window, cx| {
2051                        Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx)
2052                    })
2053                    .on_click(cx.listener(|this, _, window, cx| {
2054                        this.toggle_filters(window, cx);
2055                    }))
2056                    .toggle_state(
2057                        self.active_project_search
2058                            .as_ref()
2059                            .map(|search| search.read(cx).filters_enabled)
2060                            .unwrap_or_default(),
2061                    )
2062                    .tooltip({
2063                        let focus_handle = focus_handle.clone();
2064                        move |window, cx| {
2065                            Tooltip::for_action_in(
2066                                "Toggle Filters",
2067                                &ToggleFilters,
2068                                &focus_handle,
2069                                window,
2070                                cx,
2071                            )
2072                        }
2073                    }),
2074            )
2075            .child(render_action_button(
2076                "project-search",
2077                IconName::Replace,
2078                self.active_project_search
2079                    .as_ref()
2080                    .map(|search| search.read(cx).replace_enabled)
2081                    .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)),
2082                "Toggle Replace",
2083                &ToggleReplace,
2084                focus_handle.clone(),
2085            ))
2086            .child(matches_column);
2087
2088        let search_line = h_flex()
2089            .w_full()
2090            .gap_2()
2091            .child(query_column)
2092            .child(mode_column);
2093
2094        let replace_line = search.replace_enabled.then(|| {
2095            let replace_column = input_base_styles(InputPanel::Replacement)
2096                .child(render_text_input(&search.replacement_editor, None, cx));
2097
2098            let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
2099
2100            let replace_actions = h_flex()
2101                .min_w_64()
2102                .gap_1()
2103                .child(render_action_button(
2104                    "project-search-replace-button",
2105                    IconName::ReplaceNext,
2106                    Default::default(),
2107                    "Replace Next Match",
2108                    &ReplaceNext,
2109                    focus_handle.clone(),
2110                ))
2111                .child(render_action_button(
2112                    "project-search-replace-button",
2113                    IconName::ReplaceAll,
2114                    Default::default(),
2115                    "Replace All Matches",
2116                    &ReplaceAll,
2117                    focus_handle,
2118                ));
2119
2120            h_flex()
2121                .w_full()
2122                .gap_2()
2123                .child(replace_column)
2124                .child(replace_actions)
2125        });
2126
2127        let filter_line = search.filters_enabled.then(|| {
2128            let include = input_base_styles(InputPanel::Include)
2129                .on_action(cx.listener(|this, action, window, cx| {
2130                    this.previous_history_query(action, window, cx)
2131                }))
2132                .on_action(cx.listener(|this, action, window, cx| {
2133                    this.next_history_query(action, window, cx)
2134                }))
2135                .child(render_text_input(&search.included_files_editor, None, cx));
2136            let exclude = input_base_styles(InputPanel::Exclude)
2137                .on_action(cx.listener(|this, action, window, cx| {
2138                    this.previous_history_query(action, window, cx)
2139                }))
2140                .on_action(cx.listener(|this, action, window, cx| {
2141                    this.next_history_query(action, window, cx)
2142                }))
2143                .child(render_text_input(&search.excluded_files_editor, None, cx));
2144            let mode_column = h_flex()
2145                .gap_1()
2146                .min_w_64()
2147                .child(
2148                    IconButton::new("project-search-opened-only", IconName::FolderSearch)
2149                        .shape(IconButtonShape::Square)
2150                        .toggle_state(self.is_opened_only_enabled(cx))
2151                        .tooltip(Tooltip::text("Only Search Open Files"))
2152                        .on_click(cx.listener(|this, _, window, cx| {
2153                            this.toggle_opened_only(window, cx);
2154                        })),
2155                )
2156                .child(SearchOption::IncludeIgnored.as_button(
2157                    search.search_options,
2158                    SearchSource::Project(cx),
2159                    focus_handle.clone(),
2160                ));
2161            h_flex()
2162                .w_full()
2163                .gap_2()
2164                .child(
2165                    h_flex()
2166                        .gap_2()
2167                        .w(input_width)
2168                        .child(include)
2169                        .child(exclude),
2170                )
2171                .child(mode_column)
2172        });
2173
2174        let mut key_context = KeyContext::default();
2175        key_context.add("ProjectSearchBar");
2176        if search
2177            .replacement_editor
2178            .focus_handle(cx)
2179            .is_focused(window)
2180        {
2181            key_context.add("in_replace");
2182        }
2183
2184        let query_error_line = search
2185            .panels_with_errors
2186            .get(&InputPanel::Query)
2187            .map(|error| {
2188                Label::new(error)
2189                    .size(LabelSize::Small)
2190                    .color(Color::Error)
2191                    .mt_neg_1()
2192                    .ml_2()
2193            });
2194
2195        let filter_error_line = search
2196            .panels_with_errors
2197            .get(&InputPanel::Include)
2198            .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude))
2199            .map(|error| {
2200                Label::new(error)
2201                    .size(LabelSize::Small)
2202                    .color(Color::Error)
2203                    .mt_neg_1()
2204                    .ml_2()
2205            });
2206
2207        v_flex()
2208            .gap_2()
2209            .py(px(1.0))
2210            .w_full()
2211            .key_context(key_context)
2212            .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| {
2213                this.move_focus_to_results(window, cx)
2214            }))
2215            .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| {
2216                this.toggle_filters(window, cx);
2217            }))
2218            .capture_action(cx.listener(Self::tab))
2219            .capture_action(cx.listener(Self::backtab))
2220            .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
2221            .on_action(cx.listener(|this, action, window, cx| {
2222                this.toggle_replace(action, window, cx);
2223            }))
2224            .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| {
2225                this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
2226            }))
2227            .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| {
2228                this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
2229            }))
2230            .on_action(cx.listener(|this, action, window, cx| {
2231                if let Some(search) = this.active_project_search.as_ref() {
2232                    search.update(cx, |this, cx| {
2233                        this.replace_next(action, window, cx);
2234                    })
2235                }
2236            }))
2237            .on_action(cx.listener(|this, action, window, cx| {
2238                if let Some(search) = this.active_project_search.as_ref() {
2239                    search.update(cx, |this, cx| {
2240                        this.replace_all(action, window, cx);
2241                    })
2242                }
2243            }))
2244            .when(search.filters_enabled, |this| {
2245                this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| {
2246                    this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
2247                }))
2248            })
2249            .on_action(cx.listener(Self::select_next_match))
2250            .on_action(cx.listener(Self::select_prev_match))
2251            .child(search_line)
2252            .children(query_error_line)
2253            .children(replace_line)
2254            .children(filter_line)
2255            .children(filter_error_line)
2256    }
2257}
2258
2259impl EventEmitter<ToolbarItemEvent> for ProjectSearchBar {}
2260
2261impl ToolbarItemView for ProjectSearchBar {
2262    fn set_active_pane_item(
2263        &mut self,
2264        active_pane_item: Option<&dyn ItemHandle>,
2265        _: &mut Window,
2266        cx: &mut Context<Self>,
2267    ) -> ToolbarItemLocation {
2268        cx.notify();
2269        self.subscription = None;
2270        self.active_project_search = None;
2271        if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
2272            self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
2273            self.active_project_search = Some(search);
2274            ToolbarItemLocation::PrimaryLeft {}
2275        } else {
2276            ToolbarItemLocation::Hidden
2277        }
2278    }
2279}
2280
2281fn register_workspace_action<A: Action>(
2282    workspace: &mut Workspace,
2283    callback: fn(&mut ProjectSearchBar, &A, &mut Window, &mut Context<ProjectSearchBar>),
2284) {
2285    workspace.register_action(move |workspace, action: &A, window, cx| {
2286        if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
2287            cx.propagate();
2288            return;
2289        }
2290
2291        workspace.active_pane().update(cx, |pane, cx| {
2292            pane.toolbar().update(cx, move |workspace, cx| {
2293                if let Some(search_bar) = workspace.item_of_type::<ProjectSearchBar>() {
2294                    search_bar.update(cx, move |search_bar, cx| {
2295                        if search_bar.active_project_search.is_some() {
2296                            callback(search_bar, action, window, cx);
2297                            cx.notify();
2298                        } else {
2299                            cx.propagate();
2300                        }
2301                    });
2302                }
2303            });
2304        })
2305    });
2306}
2307
2308fn register_workspace_action_for_present_search<A: Action>(
2309    workspace: &mut Workspace,
2310    callback: fn(&mut Workspace, &A, &mut Window, &mut Context<Workspace>),
2311) {
2312    workspace.register_action(move |workspace, action: &A, window, cx| {
2313        if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
2314            cx.propagate();
2315            return;
2316        }
2317
2318        let should_notify = workspace
2319            .active_pane()
2320            .read(cx)
2321            .toolbar()
2322            .read(cx)
2323            .item_of_type::<ProjectSearchBar>()
2324            .map(|search_bar| search_bar.read(cx).active_project_search.is_some())
2325            .unwrap_or(false);
2326        if should_notify {
2327            callback(workspace, action, window, cx);
2328            cx.notify();
2329        } else {
2330            cx.propagate();
2331        }
2332    });
2333}
2334
2335#[cfg(any(test, feature = "test-support"))]
2336pub fn perform_project_search(
2337    search_view: &Entity<ProjectSearchView>,
2338    text: impl Into<std::sync::Arc<str>>,
2339    cx: &mut gpui::VisualTestContext,
2340) {
2341    cx.run_until_parked();
2342    search_view.update_in(cx, |search_view, window, cx| {
2343        search_view.query_editor.update(cx, |query_editor, cx| {
2344            query_editor.set_text(text, window, cx)
2345        });
2346        search_view.search(cx);
2347    });
2348    cx.run_until_parked();
2349}
2350
2351#[cfg(test)]
2352pub mod tests {
2353    use std::{ops::Deref as _, sync::Arc, time::Duration};
2354
2355    use super::*;
2356    use editor::{DisplayPoint, display_map::DisplayRow};
2357    use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
2358    use project::FakeFs;
2359    use serde_json::json;
2360    use settings::SettingsStore;
2361    use util::{path, paths::PathStyle, rel_path::rel_path};
2362    use util_macros::perf;
2363    use workspace::DeploySearch;
2364
2365    #[perf]
2366    #[gpui::test]
2367    async fn test_project_search(cx: &mut TestAppContext) {
2368        init_test(cx);
2369
2370        let fs = FakeFs::new(cx.background_executor.clone());
2371        fs.insert_tree(
2372            path!("/dir"),
2373            json!({
2374                "one.rs": "const ONE: usize = 1;",
2375                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2376                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2377                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2378            }),
2379        )
2380        .await;
2381        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2382        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2383        let workspace = window.root(cx).unwrap();
2384        let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
2385        let search_view = cx.add_window(|window, cx| {
2386            ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
2387        });
2388
2389        perform_search(search_view, "TWO", cx);
2390        search_view.update(cx, |search_view, window, cx| {
2391            assert_eq!(
2392                search_view
2393                    .results_editor
2394                    .update(cx, |editor, cx| editor.display_text(cx)),
2395                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
2396            );
2397            let match_background_color = cx.theme().colors().search_match_background;
2398            let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background;
2399            assert_eq!(
2400                search_view
2401                    .results_editor
2402                    .update(cx, |editor, cx| editor.all_text_background_highlights(window, cx)),
2403                &[
2404                    (
2405                        DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35),
2406                        match_background_color
2407                    ),
2408                    (
2409                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
2410                        selection_background_color
2411                    ),
2412                    (
2413                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
2414                        match_background_color
2415                    ),
2416                    (
2417                        DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
2418                        selection_background_color
2419                    ),
2420                    (
2421                        DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
2422                        match_background_color
2423                    ),
2424
2425                ]
2426            );
2427            assert_eq!(search_view.active_match_index, Some(0));
2428            assert_eq!(
2429                search_view
2430                    .results_editor
2431                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2432                [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
2433            );
2434
2435            search_view.select_match(Direction::Next, window, cx);
2436        }).unwrap();
2437
2438        search_view
2439            .update(cx, |search_view, window, cx| {
2440                assert_eq!(search_view.active_match_index, Some(1));
2441                assert_eq!(
2442                    search_view
2443                        .results_editor
2444                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2445                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
2446                );
2447                search_view.select_match(Direction::Next, window, cx);
2448            })
2449            .unwrap();
2450
2451        search_view
2452            .update(cx, |search_view, window, cx| {
2453                assert_eq!(search_view.active_match_index, Some(2));
2454                assert_eq!(
2455                    search_view
2456                        .results_editor
2457                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2458                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
2459                );
2460                search_view.select_match(Direction::Next, window, cx);
2461            })
2462            .unwrap();
2463
2464        search_view
2465            .update(cx, |search_view, window, cx| {
2466                assert_eq!(search_view.active_match_index, Some(0));
2467                assert_eq!(
2468                    search_view
2469                        .results_editor
2470                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2471                    [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
2472                );
2473                search_view.select_match(Direction::Prev, window, cx);
2474            })
2475            .unwrap();
2476
2477        search_view
2478            .update(cx, |search_view, window, cx| {
2479                assert_eq!(search_view.active_match_index, Some(2));
2480                assert_eq!(
2481                    search_view
2482                        .results_editor
2483                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2484                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
2485                );
2486                search_view.select_match(Direction::Prev, window, cx);
2487            })
2488            .unwrap();
2489
2490        search_view
2491            .update(cx, |search_view, _, cx| {
2492                assert_eq!(search_view.active_match_index, Some(1));
2493                assert_eq!(
2494                    search_view
2495                        .results_editor
2496                        .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
2497                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
2498                );
2499            })
2500            .unwrap();
2501    }
2502
2503    #[perf]
2504    #[gpui::test]
2505    async fn test_deploy_project_search_focus(cx: &mut TestAppContext) {
2506        init_test(cx);
2507
2508        let fs = FakeFs::new(cx.background_executor.clone());
2509        fs.insert_tree(
2510            "/dir",
2511            json!({
2512                "one.rs": "const ONE: usize = 1;",
2513                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2514                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2515                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2516            }),
2517        )
2518        .await;
2519        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2520        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2521        let workspace = window;
2522        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2523
2524        let active_item = cx.read(|cx| {
2525            workspace
2526                .read(cx)
2527                .unwrap()
2528                .active_pane()
2529                .read(cx)
2530                .active_item()
2531                .and_then(|item| item.downcast::<ProjectSearchView>())
2532        });
2533        assert!(
2534            active_item.is_none(),
2535            "Expected no search panel to be active"
2536        );
2537
2538        window
2539            .update(cx, move |workspace, window, cx| {
2540                assert_eq!(workspace.panes().len(), 1);
2541                workspace.panes()[0].update(cx, |pane, cx| {
2542                    pane.toolbar()
2543                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2544                });
2545
2546                ProjectSearchView::deploy_search(
2547                    workspace,
2548                    &workspace::DeploySearch::find(),
2549                    window,
2550                    cx,
2551                )
2552            })
2553            .unwrap();
2554
2555        let Some(search_view) = cx.read(|cx| {
2556            workspace
2557                .read(cx)
2558                .unwrap()
2559                .active_pane()
2560                .read(cx)
2561                .active_item()
2562                .and_then(|item| item.downcast::<ProjectSearchView>())
2563        }) else {
2564            panic!("Search view expected to appear after new search event trigger")
2565        };
2566
2567        cx.spawn(|mut cx| async move {
2568            window
2569                .update(&mut cx, |_, window, cx| {
2570                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2571                })
2572                .unwrap();
2573        })
2574        .detach();
2575        cx.background_executor.run_until_parked();
2576        window
2577            .update(cx, |_, window, cx| {
2578                search_view.update(cx, |search_view, cx| {
2579                    assert!(
2580                        search_view.query_editor.focus_handle(cx).is_focused(window),
2581                        "Empty search view should be focused after the toggle focus event: no results panel to focus on",
2582                    );
2583                });
2584        }).unwrap();
2585
2586        window
2587            .update(cx, |_, window, cx| {
2588                search_view.update(cx, |search_view, cx| {
2589                    let query_editor = &search_view.query_editor;
2590                    assert!(
2591                        query_editor.focus_handle(cx).is_focused(window),
2592                        "Search view should be focused after the new search view is activated",
2593                    );
2594                    let query_text = query_editor.read(cx).text(cx);
2595                    assert!(
2596                        query_text.is_empty(),
2597                        "New search query should be empty but got '{query_text}'",
2598                    );
2599                    let results_text = search_view
2600                        .results_editor
2601                        .update(cx, |editor, cx| editor.display_text(cx));
2602                    assert!(
2603                        results_text.is_empty(),
2604                        "Empty search view should have no results but got '{results_text}'"
2605                    );
2606                });
2607            })
2608            .unwrap();
2609
2610        window
2611            .update(cx, |_, window, cx| {
2612                search_view.update(cx, |search_view, cx| {
2613                    search_view.query_editor.update(cx, |query_editor, cx| {
2614                        query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
2615                    });
2616                    search_view.search(cx);
2617                });
2618            })
2619            .unwrap();
2620        cx.background_executor.run_until_parked();
2621        window
2622            .update(cx, |_, window, cx| {
2623                search_view.update(cx, |search_view, cx| {
2624                    let results_text = search_view
2625                        .results_editor
2626                        .update(cx, |editor, cx| editor.display_text(cx));
2627                    assert!(
2628                        results_text.is_empty(),
2629                        "Search view for mismatching query should have no results but got '{results_text}'"
2630                    );
2631                    assert!(
2632                        search_view.query_editor.focus_handle(cx).is_focused(window),
2633                        "Search view should be focused after mismatching query had been used in search",
2634                    );
2635                });
2636            }).unwrap();
2637
2638        cx.spawn(|mut cx| async move {
2639            window.update(&mut cx, |_, window, cx| {
2640                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2641            })
2642        })
2643        .detach();
2644        cx.background_executor.run_until_parked();
2645        window.update(cx, |_, window, cx| {
2646            search_view.update(cx, |search_view, cx| {
2647                assert!(
2648                    search_view.query_editor.focus_handle(cx).is_focused(window),
2649                    "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
2650                );
2651            });
2652        }).unwrap();
2653
2654        window
2655            .update(cx, |_, window, cx| {
2656                search_view.update(cx, |search_view, cx| {
2657                    search_view.query_editor.update(cx, |query_editor, cx| {
2658                        query_editor.set_text("TWO", window, cx)
2659                    });
2660                    search_view.search(cx);
2661                });
2662            })
2663            .unwrap();
2664        cx.background_executor.run_until_parked();
2665        window.update(cx, |_, window, cx| {
2666            search_view.update(cx, |search_view, cx| {
2667                assert_eq!(
2668                    search_view
2669                        .results_editor
2670                        .update(cx, |editor, cx| editor.display_text(cx)),
2671                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2672                    "Search view results should match the query"
2673                );
2674                assert!(
2675                    search_view.results_editor.focus_handle(cx).is_focused(window),
2676                    "Search view with mismatching query should be focused after search results are available",
2677                );
2678            });
2679        }).unwrap();
2680        cx.spawn(|mut cx| async move {
2681            window
2682                .update(&mut cx, |_, window, cx| {
2683                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2684                })
2685                .unwrap();
2686        })
2687        .detach();
2688        cx.background_executor.run_until_parked();
2689        window.update(cx, |_, window, cx| {
2690            search_view.update(cx, |search_view, cx| {
2691                assert!(
2692                    search_view.results_editor.focus_handle(cx).is_focused(window),
2693                    "Search view with matching query should still have its results editor focused after the toggle focus event",
2694                );
2695            });
2696        }).unwrap();
2697
2698        workspace
2699            .update(cx, |workspace, window, cx| {
2700                ProjectSearchView::deploy_search(
2701                    workspace,
2702                    &workspace::DeploySearch::find(),
2703                    window,
2704                    cx,
2705                )
2706            })
2707            .unwrap();
2708        window.update(cx, |_, window, cx| {
2709            search_view.update(cx, |search_view, cx| {
2710                assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
2711                assert_eq!(
2712                    search_view
2713                        .results_editor
2714                        .update(cx, |editor, cx| editor.display_text(cx)),
2715                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
2716                    "Results should be unchanged after search view 2nd open in a row"
2717                );
2718                assert!(
2719                    search_view.query_editor.focus_handle(cx).is_focused(window),
2720                    "Focus should be moved into query editor again after search view 2nd open in a row"
2721                );
2722            });
2723        }).unwrap();
2724
2725        cx.spawn(|mut cx| async move {
2726            window
2727                .update(&mut cx, |_, window, cx| {
2728                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2729                })
2730                .unwrap();
2731        })
2732        .detach();
2733        cx.background_executor.run_until_parked();
2734        window.update(cx, |_, window, cx| {
2735            search_view.update(cx, |search_view, cx| {
2736                assert!(
2737                    search_view.results_editor.focus_handle(cx).is_focused(window),
2738                    "Search view with matching query should switch focus to the results editor after the toggle focus event",
2739                );
2740            });
2741        }).unwrap();
2742    }
2743
2744    #[perf]
2745    #[gpui::test]
2746    async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) {
2747        init_test(cx);
2748
2749        let fs = FakeFs::new(cx.background_executor.clone());
2750        fs.insert_tree(
2751            "/dir",
2752            json!({
2753                "one.rs": "const ONE: usize = 1;",
2754                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2755                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2756                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2757            }),
2758        )
2759        .await;
2760        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2761        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2762        let workspace = window;
2763        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2764
2765        window
2766            .update(cx, move |workspace, window, cx| {
2767                workspace.panes()[0].update(cx, |pane, cx| {
2768                    pane.toolbar()
2769                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2770                });
2771
2772                ProjectSearchView::deploy_search(
2773                    workspace,
2774                    &workspace::DeploySearch::find(),
2775                    window,
2776                    cx,
2777                )
2778            })
2779            .unwrap();
2780
2781        let Some(search_view) = cx.read(|cx| {
2782            workspace
2783                .read(cx)
2784                .unwrap()
2785                .active_pane()
2786                .read(cx)
2787                .active_item()
2788                .and_then(|item| item.downcast::<ProjectSearchView>())
2789        }) else {
2790            panic!("Search view expected to appear after new search event trigger")
2791        };
2792
2793        cx.spawn(|mut cx| async move {
2794            window
2795                .update(&mut cx, |_, window, cx| {
2796                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2797                })
2798                .unwrap();
2799        })
2800        .detach();
2801        cx.background_executor.run_until_parked();
2802
2803        window
2804            .update(cx, |_, window, cx| {
2805                search_view.update(cx, |search_view, cx| {
2806                    search_view.query_editor.update(cx, |query_editor, cx| {
2807                        query_editor.set_text("const FOUR", window, cx)
2808                    });
2809                    search_view.toggle_filters(cx);
2810                    search_view
2811                        .excluded_files_editor
2812                        .update(cx, |exclude_editor, cx| {
2813                            exclude_editor.set_text("four.rs", window, cx)
2814                        });
2815                    search_view.search(cx);
2816                });
2817            })
2818            .unwrap();
2819        cx.background_executor.run_until_parked();
2820        window
2821            .update(cx, |_, _, cx| {
2822                search_view.update(cx, |search_view, cx| {
2823                    let results_text = search_view
2824                        .results_editor
2825                        .update(cx, |editor, cx| editor.display_text(cx));
2826                    assert!(
2827                        results_text.is_empty(),
2828                        "Search view for query with the only match in an excluded file should have no results but got '{results_text}'"
2829                    );
2830                });
2831            }).unwrap();
2832
2833        cx.spawn(|mut cx| async move {
2834            window.update(&mut cx, |_, window, cx| {
2835                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2836            })
2837        })
2838        .detach();
2839        cx.background_executor.run_until_parked();
2840
2841        window
2842            .update(cx, |_, _, cx| {
2843                search_view.update(cx, |search_view, cx| {
2844                    search_view.toggle_filters(cx);
2845                    search_view.search(cx);
2846                });
2847            })
2848            .unwrap();
2849        cx.background_executor.run_until_parked();
2850        window
2851            .update(cx, |_, _, cx| {
2852                search_view.update(cx, |search_view, cx| {
2853                assert_eq!(
2854                    search_view
2855                        .results_editor
2856                        .update(cx, |editor, cx| editor.display_text(cx)),
2857                    "\n\nconst FOUR: usize = one::ONE + three::THREE;",
2858                    "Search view results should contain the queried result in the previously excluded file with filters toggled off"
2859                );
2860            });
2861            })
2862            .unwrap();
2863    }
2864
2865    #[perf]
2866    #[gpui::test]
2867    async fn test_new_project_search_focus(cx: &mut TestAppContext) {
2868        init_test(cx);
2869
2870        let fs = FakeFs::new(cx.background_executor.clone());
2871        fs.insert_tree(
2872            path!("/dir"),
2873            json!({
2874                "one.rs": "const ONE: usize = 1;",
2875                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2876                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
2877                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
2878            }),
2879        )
2880        .await;
2881        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2882        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2883        let workspace = window;
2884        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
2885
2886        let active_item = cx.read(|cx| {
2887            workspace
2888                .read(cx)
2889                .unwrap()
2890                .active_pane()
2891                .read(cx)
2892                .active_item()
2893                .and_then(|item| item.downcast::<ProjectSearchView>())
2894        });
2895        assert!(
2896            active_item.is_none(),
2897            "Expected no search panel to be active"
2898        );
2899
2900        window
2901            .update(cx, move |workspace, window, cx| {
2902                assert_eq!(workspace.panes().len(), 1);
2903                workspace.panes()[0].update(cx, |pane, cx| {
2904                    pane.toolbar()
2905                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
2906                });
2907
2908                ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
2909            })
2910            .unwrap();
2911
2912        let Some(search_view) = cx.read(|cx| {
2913            workspace
2914                .read(cx)
2915                .unwrap()
2916                .active_pane()
2917                .read(cx)
2918                .active_item()
2919                .and_then(|item| item.downcast::<ProjectSearchView>())
2920        }) else {
2921            panic!("Search view expected to appear after new search event trigger")
2922        };
2923
2924        cx.spawn(|mut cx| async move {
2925            window
2926                .update(&mut cx, |_, window, cx| {
2927                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2928                })
2929                .unwrap();
2930        })
2931        .detach();
2932        cx.background_executor.run_until_parked();
2933
2934        window.update(cx, |_, window, cx| {
2935            search_view.update(cx, |search_view, cx| {
2936                    assert!(
2937                        search_view.query_editor.focus_handle(cx).is_focused(window),
2938                        "Empty search view should be focused after the toggle focus event: no results panel to focus on",
2939                    );
2940                });
2941        }).unwrap();
2942
2943        window
2944            .update(cx, |_, window, cx| {
2945                search_view.update(cx, |search_view, cx| {
2946                    let query_editor = &search_view.query_editor;
2947                    assert!(
2948                        query_editor.focus_handle(cx).is_focused(window),
2949                        "Search view should be focused after the new search view is activated",
2950                    );
2951                    let query_text = query_editor.read(cx).text(cx);
2952                    assert!(
2953                        query_text.is_empty(),
2954                        "New search query should be empty but got '{query_text}'",
2955                    );
2956                    let results_text = search_view
2957                        .results_editor
2958                        .update(cx, |editor, cx| editor.display_text(cx));
2959                    assert!(
2960                        results_text.is_empty(),
2961                        "Empty search view should have no results but got '{results_text}'"
2962                    );
2963                });
2964            })
2965            .unwrap();
2966
2967        window
2968            .update(cx, |_, window, cx| {
2969                search_view.update(cx, |search_view, cx| {
2970                    search_view.query_editor.update(cx, |query_editor, cx| {
2971                        query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx)
2972                    });
2973                    search_view.search(cx);
2974                });
2975            })
2976            .unwrap();
2977
2978        cx.background_executor.run_until_parked();
2979        window
2980            .update(cx, |_, window, cx| {
2981                search_view.update(cx, |search_view, cx| {
2982                    let results_text = search_view
2983                        .results_editor
2984                        .update(cx, |editor, cx| editor.display_text(cx));
2985                    assert!(
2986                results_text.is_empty(),
2987                "Search view for mismatching query should have no results but got '{results_text}'"
2988            );
2989                    assert!(
2990                search_view.query_editor.focus_handle(cx).is_focused(window),
2991                "Search view should be focused after mismatching query had been used in search",
2992            );
2993                });
2994            })
2995            .unwrap();
2996        cx.spawn(|mut cx| async move {
2997            window.update(&mut cx, |_, window, cx| {
2998                window.dispatch_action(ToggleFocus.boxed_clone(), cx)
2999            })
3000        })
3001        .detach();
3002        cx.background_executor.run_until_parked();
3003        window.update(cx, |_, window, cx| {
3004            search_view.update(cx, |search_view, cx| {
3005                    assert!(
3006                        search_view.query_editor.focus_handle(cx).is_focused(window),
3007                        "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
3008                    );
3009                });
3010        }).unwrap();
3011
3012        window
3013            .update(cx, |_, window, cx| {
3014                search_view.update(cx, |search_view, cx| {
3015                    search_view.query_editor.update(cx, |query_editor, cx| {
3016                        query_editor.set_text("TWO", window, cx)
3017                    });
3018                    search_view.search(cx);
3019                })
3020            })
3021            .unwrap();
3022        cx.background_executor.run_until_parked();
3023        window.update(cx, |_, window, cx|
3024        search_view.update(cx, |search_view, cx| {
3025                assert_eq!(
3026                    search_view
3027                        .results_editor
3028                        .update(cx, |editor, cx| editor.display_text(cx)),
3029                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3030                    "Search view results should match the query"
3031                );
3032                assert!(
3033                    search_view.results_editor.focus_handle(cx).is_focused(window),
3034                    "Search view with mismatching query should be focused after search results are available",
3035                );
3036            })).unwrap();
3037        cx.spawn(|mut cx| async move {
3038            window
3039                .update(&mut cx, |_, window, cx| {
3040                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
3041                })
3042                .unwrap();
3043        })
3044        .detach();
3045        cx.background_executor.run_until_parked();
3046        window.update(cx, |_, window, cx| {
3047            search_view.update(cx, |search_view, cx| {
3048                    assert!(
3049                        search_view.results_editor.focus_handle(cx).is_focused(window),
3050                        "Search view with matching query should still have its results editor focused after the toggle focus event",
3051                    );
3052                });
3053        }).unwrap();
3054
3055        workspace
3056            .update(cx, |workspace, window, cx| {
3057                ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3058            })
3059            .unwrap();
3060        cx.background_executor.run_until_parked();
3061        let Some(search_view_2) = cx.read(|cx| {
3062            workspace
3063                .read(cx)
3064                .unwrap()
3065                .active_pane()
3066                .read(cx)
3067                .active_item()
3068                .and_then(|item| item.downcast::<ProjectSearchView>())
3069        }) else {
3070            panic!("Search view expected to appear after new search event trigger")
3071        };
3072        assert!(
3073            search_view_2 != search_view,
3074            "New search view should be open after `workspace::NewSearch` event"
3075        );
3076
3077        window.update(cx, |_, window, cx| {
3078            search_view.update(cx, |search_view, cx| {
3079                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
3080                    assert_eq!(
3081                        search_view
3082                            .results_editor
3083                            .update(cx, |editor, cx| editor.display_text(cx)),
3084                        "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3085                        "Results of the first search view should not update too"
3086                    );
3087                    assert!(
3088                        !search_view.query_editor.focus_handle(cx).is_focused(window),
3089                        "Focus should be moved away from the first search view"
3090                    );
3091                });
3092        }).unwrap();
3093
3094        window.update(cx, |_, window, cx| {
3095            search_view_2.update(cx, |search_view_2, cx| {
3096                    assert_eq!(
3097                        search_view_2.query_editor.read(cx).text(cx),
3098                        "two",
3099                        "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
3100                    );
3101                    assert_eq!(
3102                        search_view_2
3103                            .results_editor
3104                            .update(cx, |editor, cx| editor.display_text(cx)),
3105                        "",
3106                        "No search results should be in the 2nd view yet, as we did not spawn a search for it"
3107                    );
3108                    assert!(
3109                        search_view_2.query_editor.focus_handle(cx).is_focused(window),
3110                        "Focus should be moved into query editor of the new window"
3111                    );
3112                });
3113        }).unwrap();
3114
3115        window
3116            .update(cx, |_, window, cx| {
3117                search_view_2.update(cx, |search_view_2, cx| {
3118                    search_view_2.query_editor.update(cx, |query_editor, cx| {
3119                        query_editor.set_text("FOUR", window, cx)
3120                    });
3121                    search_view_2.search(cx);
3122                });
3123            })
3124            .unwrap();
3125
3126        cx.background_executor.run_until_parked();
3127        window.update(cx, |_, window, cx| {
3128            search_view_2.update(cx, |search_view_2, cx| {
3129                    assert_eq!(
3130                        search_view_2
3131                            .results_editor
3132                            .update(cx, |editor, cx| editor.display_text(cx)),
3133                        "\n\nconst FOUR: usize = one::ONE + three::THREE;",
3134                        "New search view with the updated query should have new search results"
3135                    );
3136                    assert!(
3137                        search_view_2.results_editor.focus_handle(cx).is_focused(window),
3138                        "Search view with mismatching query should be focused after search results are available",
3139                    );
3140                });
3141        }).unwrap();
3142
3143        cx.spawn(|mut cx| async move {
3144            window
3145                .update(&mut cx, |_, window, cx| {
3146                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
3147                })
3148                .unwrap();
3149        })
3150        .detach();
3151        cx.background_executor.run_until_parked();
3152        window.update(cx, |_, window, cx| {
3153            search_view_2.update(cx, |search_view_2, cx| {
3154                    assert!(
3155                        search_view_2.results_editor.focus_handle(cx).is_focused(window),
3156                        "Search view with matching query should switch focus to the results editor after the toggle focus event",
3157                    );
3158                });}).unwrap();
3159    }
3160
3161    #[perf]
3162    #[gpui::test]
3163    async fn test_new_project_search_in_directory(cx: &mut TestAppContext) {
3164        init_test(cx);
3165
3166        let fs = FakeFs::new(cx.background_executor.clone());
3167        fs.insert_tree(
3168            path!("/dir"),
3169            json!({
3170                "a": {
3171                    "one.rs": "const ONE: usize = 1;",
3172                    "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3173                },
3174                "b": {
3175                    "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3176                    "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3177                },
3178            }),
3179        )
3180        .await;
3181        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3182        let worktree_id = project.read_with(cx, |project, cx| {
3183            project.worktrees(cx).next().unwrap().read(cx).id()
3184        });
3185        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3186        let workspace = window.root(cx).unwrap();
3187        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3188
3189        let active_item = cx.read(|cx| {
3190            workspace
3191                .read(cx)
3192                .active_pane()
3193                .read(cx)
3194                .active_item()
3195                .and_then(|item| item.downcast::<ProjectSearchView>())
3196        });
3197        assert!(
3198            active_item.is_none(),
3199            "Expected no search panel to be active"
3200        );
3201
3202        window
3203            .update(cx, move |workspace, window, cx| {
3204                assert_eq!(workspace.panes().len(), 1);
3205                workspace.panes()[0].update(cx, move |pane, cx| {
3206                    pane.toolbar()
3207                        .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3208                });
3209            })
3210            .unwrap();
3211
3212        let a_dir_entry = cx.update(|cx| {
3213            workspace
3214                .read(cx)
3215                .project()
3216                .read(cx)
3217                .entry_for_path(&(worktree_id, rel_path("a")).into(), cx)
3218                .expect("no entry for /a/ directory")
3219                .clone()
3220        });
3221        assert!(a_dir_entry.is_dir());
3222        window
3223            .update(cx, |workspace, window, cx| {
3224                ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx)
3225            })
3226            .unwrap();
3227
3228        let Some(search_view) = cx.read(|cx| {
3229            workspace
3230                .read(cx)
3231                .active_pane()
3232                .read(cx)
3233                .active_item()
3234                .and_then(|item| item.downcast::<ProjectSearchView>())
3235        }) else {
3236            panic!("Search view expected to appear after new search in directory event trigger")
3237        };
3238        cx.background_executor.run_until_parked();
3239        window
3240            .update(cx, |_, window, cx| {
3241                search_view.update(cx, |search_view, cx| {
3242                    assert!(
3243                        search_view.query_editor.focus_handle(cx).is_focused(window),
3244                        "On new search in directory, focus should be moved into query editor"
3245                    );
3246                    search_view.excluded_files_editor.update(cx, |editor, cx| {
3247                        assert!(
3248                            editor.display_text(cx).is_empty(),
3249                            "New search in directory should not have any excluded files"
3250                        );
3251                    });
3252                    search_view.included_files_editor.update(cx, |editor, cx| {
3253                        assert_eq!(
3254                            editor.display_text(cx),
3255                            a_dir_entry.path.display(PathStyle::local()),
3256                            "New search in directory should have included dir entry path"
3257                        );
3258                    });
3259                });
3260            })
3261            .unwrap();
3262        window
3263            .update(cx, |_, window, cx| {
3264                search_view.update(cx, |search_view, cx| {
3265                    search_view.query_editor.update(cx, |query_editor, cx| {
3266                        query_editor.set_text("const", window, cx)
3267                    });
3268                    search_view.search(cx);
3269                });
3270            })
3271            .unwrap();
3272        cx.background_executor.run_until_parked();
3273        window
3274            .update(cx, |_, _, cx| {
3275                search_view.update(cx, |search_view, cx| {
3276                    assert_eq!(
3277                search_view
3278                    .results_editor
3279                    .update(cx, |editor, cx| editor.display_text(cx)),
3280                "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
3281                "New search in directory should have a filter that matches a certain directory"
3282            );
3283                })
3284            })
3285            .unwrap();
3286    }
3287
3288    #[perf]
3289    #[gpui::test]
3290    async fn test_search_query_history(cx: &mut TestAppContext) {
3291        init_test(cx);
3292
3293        let fs = FakeFs::new(cx.background_executor.clone());
3294        fs.insert_tree(
3295            path!("/dir"),
3296            json!({
3297                "one.rs": "const ONE: usize = 1;",
3298                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3299                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3300                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3301            }),
3302        )
3303        .await;
3304        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3305        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3306        let workspace = window.root(cx).unwrap();
3307        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3308
3309        window
3310            .update(cx, {
3311                let search_bar = search_bar.clone();
3312                |workspace, window, cx| {
3313                    assert_eq!(workspace.panes().len(), 1);
3314                    workspace.panes()[0].update(cx, |pane, cx| {
3315                        pane.toolbar()
3316                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3317                    });
3318
3319                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3320                }
3321            })
3322            .unwrap();
3323
3324        let search_view = cx.read(|cx| {
3325            workspace
3326                .read(cx)
3327                .active_pane()
3328                .read(cx)
3329                .active_item()
3330                .and_then(|item| item.downcast::<ProjectSearchView>())
3331                .expect("Search view expected to appear after new search event trigger")
3332        });
3333
3334        // Add 3 search items into the history + another unsubmitted one.
3335        window
3336            .update(cx, |_, window, cx| {
3337                search_view.update(cx, |search_view, cx| {
3338                    search_view.search_options = SearchOptions::CASE_SENSITIVE;
3339                    search_view.query_editor.update(cx, |query_editor, cx| {
3340                        query_editor.set_text("ONE", window, cx)
3341                    });
3342                    search_view.search(cx);
3343                });
3344            })
3345            .unwrap();
3346
3347        cx.background_executor.run_until_parked();
3348        window
3349            .update(cx, |_, window, cx| {
3350                search_view.update(cx, |search_view, cx| {
3351                    search_view.query_editor.update(cx, |query_editor, cx| {
3352                        query_editor.set_text("TWO", window, cx)
3353                    });
3354                    search_view.search(cx);
3355                });
3356            })
3357            .unwrap();
3358        cx.background_executor.run_until_parked();
3359        window
3360            .update(cx, |_, window, cx| {
3361                search_view.update(cx, |search_view, cx| {
3362                    search_view.query_editor.update(cx, |query_editor, cx| {
3363                        query_editor.set_text("THREE", window, cx)
3364                    });
3365                    search_view.search(cx);
3366                })
3367            })
3368            .unwrap();
3369        cx.background_executor.run_until_parked();
3370        window
3371            .update(cx, |_, window, cx| {
3372                search_view.update(cx, |search_view, cx| {
3373                    search_view.query_editor.update(cx, |query_editor, cx| {
3374                        query_editor.set_text("JUST_TEXT_INPUT", window, cx)
3375                    });
3376                })
3377            })
3378            .unwrap();
3379        cx.background_executor.run_until_parked();
3380
3381        // Ensure that the latest input with search settings is active.
3382        window
3383            .update(cx, |_, _, cx| {
3384                search_view.update(cx, |search_view, cx| {
3385                    assert_eq!(
3386                        search_view.query_editor.read(cx).text(cx),
3387                        "JUST_TEXT_INPUT"
3388                    );
3389                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3390                });
3391            })
3392            .unwrap();
3393
3394        // Next history query after the latest should set the query to the empty string.
3395        window
3396            .update(cx, |_, window, cx| {
3397                search_bar.update(cx, |search_bar, cx| {
3398                    search_bar.focus_search(window, cx);
3399                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3400                })
3401            })
3402            .unwrap();
3403        window
3404            .update(cx, |_, _, cx| {
3405                search_view.update(cx, |search_view, cx| {
3406                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3407                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3408                });
3409            })
3410            .unwrap();
3411        window
3412            .update(cx, |_, window, cx| {
3413                search_bar.update(cx, |search_bar, cx| {
3414                    search_bar.focus_search(window, cx);
3415                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3416                })
3417            })
3418            .unwrap();
3419        window
3420            .update(cx, |_, _, cx| {
3421                search_view.update(cx, |search_view, cx| {
3422                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3423                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3424                });
3425            })
3426            .unwrap();
3427
3428        // First previous query for empty current query should set the query to the latest submitted one.
3429        window
3430            .update(cx, |_, window, cx| {
3431                search_bar.update(cx, |search_bar, cx| {
3432                    search_bar.focus_search(window, cx);
3433                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3434                });
3435            })
3436            .unwrap();
3437        window
3438            .update(cx, |_, _, cx| {
3439                search_view.update(cx, |search_view, cx| {
3440                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3441                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3442                });
3443            })
3444            .unwrap();
3445
3446        // Further previous items should go over the history in reverse order.
3447        window
3448            .update(cx, |_, window, cx| {
3449                search_bar.update(cx, |search_bar, cx| {
3450                    search_bar.focus_search(window, cx);
3451                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3452                });
3453            })
3454            .unwrap();
3455        window
3456            .update(cx, |_, _, cx| {
3457                search_view.update(cx, |search_view, cx| {
3458                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3459                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3460                });
3461            })
3462            .unwrap();
3463
3464        // Previous items should never go behind the first history item.
3465        window
3466            .update(cx, |_, window, cx| {
3467                search_bar.update(cx, |search_bar, cx| {
3468                    search_bar.focus_search(window, cx);
3469                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3470                });
3471            })
3472            .unwrap();
3473        window
3474            .update(cx, |_, _, cx| {
3475                search_view.update(cx, |search_view, cx| {
3476                    assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
3477                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3478                });
3479            })
3480            .unwrap();
3481        window
3482            .update(cx, |_, window, cx| {
3483                search_bar.update(cx, |search_bar, cx| {
3484                    search_bar.focus_search(window, cx);
3485                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3486                });
3487            })
3488            .unwrap();
3489        window
3490            .update(cx, |_, _, cx| {
3491                search_view.update(cx, |search_view, cx| {
3492                    assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
3493                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3494                });
3495            })
3496            .unwrap();
3497
3498        // Next items should go over the history in the original order.
3499        window
3500            .update(cx, |_, window, cx| {
3501                search_bar.update(cx, |search_bar, cx| {
3502                    search_bar.focus_search(window, cx);
3503                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3504                });
3505            })
3506            .unwrap();
3507        window
3508            .update(cx, |_, _, cx| {
3509                search_view.update(cx, |search_view, cx| {
3510                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3511                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3512                });
3513            })
3514            .unwrap();
3515
3516        window
3517            .update(cx, |_, window, cx| {
3518                search_view.update(cx, |search_view, cx| {
3519                    search_view.query_editor.update(cx, |query_editor, cx| {
3520                        query_editor.set_text("TWO_NEW", window, cx)
3521                    });
3522                    search_view.search(cx);
3523                });
3524            })
3525            .unwrap();
3526        cx.background_executor.run_until_parked();
3527        window
3528            .update(cx, |_, _, cx| {
3529                search_view.update(cx, |search_view, cx| {
3530                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
3531                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3532                });
3533            })
3534            .unwrap();
3535
3536        // New search input should add another entry to history and move the selection to the end of the history.
3537        window
3538            .update(cx, |_, window, cx| {
3539                search_bar.update(cx, |search_bar, cx| {
3540                    search_bar.focus_search(window, cx);
3541                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3542                });
3543            })
3544            .unwrap();
3545        window
3546            .update(cx, |_, _, cx| {
3547                search_view.update(cx, |search_view, cx| {
3548                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3549                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3550                });
3551            })
3552            .unwrap();
3553        window
3554            .update(cx, |_, window, cx| {
3555                search_bar.update(cx, |search_bar, cx| {
3556                    search_bar.focus_search(window, cx);
3557                    search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3558                });
3559            })
3560            .unwrap();
3561        window
3562            .update(cx, |_, _, cx| {
3563                search_view.update(cx, |search_view, cx| {
3564                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
3565                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3566                });
3567            })
3568            .unwrap();
3569        window
3570            .update(cx, |_, window, cx| {
3571                search_bar.update(cx, |search_bar, cx| {
3572                    search_bar.focus_search(window, cx);
3573                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3574                });
3575            })
3576            .unwrap();
3577        window
3578            .update(cx, |_, _, cx| {
3579                search_view.update(cx, |search_view, cx| {
3580                    assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
3581                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3582                });
3583            })
3584            .unwrap();
3585        window
3586            .update(cx, |_, window, cx| {
3587                search_bar.update(cx, |search_bar, cx| {
3588                    search_bar.focus_search(window, cx);
3589                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3590                });
3591            })
3592            .unwrap();
3593        window
3594            .update(cx, |_, _, cx| {
3595                search_view.update(cx, |search_view, cx| {
3596                    assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
3597                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3598                });
3599            })
3600            .unwrap();
3601        window
3602            .update(cx, |_, window, cx| {
3603                search_bar.update(cx, |search_bar, cx| {
3604                    search_bar.focus_search(window, cx);
3605                    search_bar.next_history_query(&NextHistoryQuery, window, cx);
3606                });
3607            })
3608            .unwrap();
3609        window
3610            .update(cx, |_, _, cx| {
3611                search_view.update(cx, |search_view, cx| {
3612                    assert_eq!(search_view.query_editor.read(cx).text(cx), "");
3613                    assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
3614                });
3615            })
3616            .unwrap();
3617    }
3618
3619    #[perf]
3620    #[gpui::test]
3621    async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) {
3622        init_test(cx);
3623
3624        let fs = FakeFs::new(cx.background_executor.clone());
3625        fs.insert_tree(
3626            path!("/dir"),
3627            json!({
3628                "one.rs": "const ONE: usize = 1;",
3629            }),
3630        )
3631        .await;
3632        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3633        let worktree_id = project.update(cx, |this, cx| {
3634            this.worktrees(cx).next().unwrap().read(cx).id()
3635        });
3636
3637        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3638        let workspace = window.root(cx).unwrap();
3639
3640        let panes: Vec<_> = window
3641            .update(cx, |this, _, _| this.panes().to_owned())
3642            .unwrap();
3643
3644        let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3645        let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3646
3647        assert_eq!(panes.len(), 1);
3648        let first_pane = panes.first().cloned().unwrap();
3649        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0);
3650        window
3651            .update(cx, |workspace, window, cx| {
3652                workspace.open_path(
3653                    (worktree_id, rel_path("one.rs")),
3654                    Some(first_pane.downgrade()),
3655                    true,
3656                    window,
3657                    cx,
3658                )
3659            })
3660            .unwrap()
3661            .await
3662            .unwrap();
3663        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3664
3665        // Add a project search item to the first pane
3666        window
3667            .update(cx, {
3668                let search_bar = search_bar_1.clone();
3669                |workspace, window, cx| {
3670                    first_pane.update(cx, |pane, cx| {
3671                        pane.toolbar()
3672                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3673                    });
3674
3675                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3676                }
3677            })
3678            .unwrap();
3679        let search_view_1 = cx.read(|cx| {
3680            workspace
3681                .read(cx)
3682                .active_item(cx)
3683                .and_then(|item| item.downcast::<ProjectSearchView>())
3684                .expect("Search view expected to appear after new search event trigger")
3685        });
3686
3687        let second_pane = window
3688            .update(cx, |workspace, window, cx| {
3689                workspace.split_and_clone(
3690                    first_pane.clone(),
3691                    workspace::SplitDirection::Right,
3692                    window,
3693                    cx,
3694                )
3695            })
3696            .unwrap()
3697            .unwrap();
3698        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3699
3700        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3701        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2);
3702
3703        // Add a project search item to the second pane
3704        window
3705            .update(cx, {
3706                let search_bar = search_bar_2.clone();
3707                let pane = second_pane.clone();
3708                move |workspace, window, cx| {
3709                    assert_eq!(workspace.panes().len(), 2);
3710                    pane.update(cx, |pane, cx| {
3711                        pane.toolbar()
3712                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3713                    });
3714
3715                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3716                }
3717            })
3718            .unwrap();
3719
3720        let search_view_2 = cx.read(|cx| {
3721            workspace
3722                .read(cx)
3723                .active_item(cx)
3724                .and_then(|item| item.downcast::<ProjectSearchView>())
3725                .expect("Search view expected to appear after new search event trigger")
3726        });
3727
3728        cx.run_until_parked();
3729        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2);
3730        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
3731
3732        let update_search_view =
3733            |search_view: &Entity<ProjectSearchView>, query: &str, cx: &mut TestAppContext| {
3734                window
3735                    .update(cx, |_, window, cx| {
3736                        search_view.update(cx, |search_view, cx| {
3737                            search_view.query_editor.update(cx, |query_editor, cx| {
3738                                query_editor.set_text(query, window, cx)
3739                            });
3740                            search_view.search(cx);
3741                        });
3742                    })
3743                    .unwrap();
3744            };
3745
3746        let active_query =
3747            |search_view: &Entity<ProjectSearchView>, cx: &mut TestAppContext| -> String {
3748                window
3749                    .update(cx, |_, _, cx| {
3750                        search_view.update(cx, |search_view, cx| {
3751                            search_view.query_editor.read(cx).text(cx)
3752                        })
3753                    })
3754                    .unwrap()
3755            };
3756
3757        let select_prev_history_item =
3758            |search_bar: &Entity<ProjectSearchBar>, cx: &mut TestAppContext| {
3759                window
3760                    .update(cx, |_, window, cx| {
3761                        search_bar.update(cx, |search_bar, cx| {
3762                            search_bar.focus_search(window, cx);
3763                            search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
3764                        })
3765                    })
3766                    .unwrap();
3767            };
3768
3769        let select_next_history_item =
3770            |search_bar: &Entity<ProjectSearchBar>, cx: &mut TestAppContext| {
3771                window
3772                    .update(cx, |_, window, cx| {
3773                        search_bar.update(cx, |search_bar, cx| {
3774                            search_bar.focus_search(window, cx);
3775                            search_bar.next_history_query(&NextHistoryQuery, window, cx);
3776                        })
3777                    })
3778                    .unwrap();
3779            };
3780
3781        update_search_view(&search_view_1, "ONE", cx);
3782        cx.background_executor.run_until_parked();
3783
3784        update_search_view(&search_view_2, "TWO", cx);
3785        cx.background_executor.run_until_parked();
3786
3787        assert_eq!(active_query(&search_view_1, cx), "ONE");
3788        assert_eq!(active_query(&search_view_2, cx), "TWO");
3789
3790        // Selecting previous history item should select the query from search view 1.
3791        select_prev_history_item(&search_bar_2, cx);
3792        assert_eq!(active_query(&search_view_2, cx), "ONE");
3793
3794        // Selecting the previous history item should not change the query as it is already the first item.
3795        select_prev_history_item(&search_bar_2, cx);
3796        assert_eq!(active_query(&search_view_2, cx), "ONE");
3797
3798        // Changing the query in search view 2 should not affect the history of search view 1.
3799        assert_eq!(active_query(&search_view_1, cx), "ONE");
3800
3801        // Deploying a new search in search view 2
3802        update_search_view(&search_view_2, "THREE", cx);
3803        cx.background_executor.run_until_parked();
3804
3805        select_next_history_item(&search_bar_2, cx);
3806        assert_eq!(active_query(&search_view_2, cx), "");
3807
3808        select_prev_history_item(&search_bar_2, cx);
3809        assert_eq!(active_query(&search_view_2, cx), "THREE");
3810
3811        select_prev_history_item(&search_bar_2, cx);
3812        assert_eq!(active_query(&search_view_2, cx), "TWO");
3813
3814        select_prev_history_item(&search_bar_2, cx);
3815        assert_eq!(active_query(&search_view_2, cx), "ONE");
3816
3817        select_prev_history_item(&search_bar_2, cx);
3818        assert_eq!(active_query(&search_view_2, cx), "ONE");
3819
3820        // Search view 1 should now see the query from search view 2.
3821        assert_eq!(active_query(&search_view_1, cx), "ONE");
3822
3823        select_next_history_item(&search_bar_2, cx);
3824        assert_eq!(active_query(&search_view_2, cx), "TWO");
3825
3826        // Here is the new query from search view 2
3827        select_next_history_item(&search_bar_2, cx);
3828        assert_eq!(active_query(&search_view_2, cx), "THREE");
3829
3830        select_next_history_item(&search_bar_2, cx);
3831        assert_eq!(active_query(&search_view_2, cx), "");
3832
3833        select_next_history_item(&search_bar_1, cx);
3834        assert_eq!(active_query(&search_view_1, cx), "TWO");
3835
3836        select_next_history_item(&search_bar_1, cx);
3837        assert_eq!(active_query(&search_view_1, cx), "THREE");
3838
3839        select_next_history_item(&search_bar_1, cx);
3840        assert_eq!(active_query(&search_view_1, cx), "");
3841    }
3842
3843    #[perf]
3844    #[gpui::test]
3845    async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) {
3846        init_test(cx);
3847
3848        // Setup 2 panes, both with a file open and one with a project search.
3849        let fs = FakeFs::new(cx.background_executor.clone());
3850        fs.insert_tree(
3851            path!("/dir"),
3852            json!({
3853                "one.rs": "const ONE: usize = 1;",
3854            }),
3855        )
3856        .await;
3857        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3858        let worktree_id = project.update(cx, |this, cx| {
3859            this.worktrees(cx).next().unwrap().read(cx).id()
3860        });
3861        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3862        let panes: Vec<_> = window
3863            .update(cx, |this, _, _| this.panes().to_owned())
3864            .unwrap();
3865        assert_eq!(panes.len(), 1);
3866        let first_pane = panes.first().cloned().unwrap();
3867        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0);
3868        window
3869            .update(cx, |workspace, window, cx| {
3870                workspace.open_path(
3871                    (worktree_id, rel_path("one.rs")),
3872                    Some(first_pane.downgrade()),
3873                    true,
3874                    window,
3875                    cx,
3876                )
3877            })
3878            .unwrap()
3879            .await
3880            .unwrap();
3881        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3882        let second_pane = window
3883            .update(cx, |workspace, window, cx| {
3884                workspace.split_and_clone(
3885                    first_pane.clone(),
3886                    workspace::SplitDirection::Right,
3887                    window,
3888                    cx,
3889                )
3890            })
3891            .unwrap()
3892            .unwrap();
3893        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
3894        assert!(
3895            window
3896                .update(cx, |_, window, cx| second_pane
3897                    .focus_handle(cx)
3898                    .contains_focused(window, cx))
3899                .unwrap()
3900        );
3901        let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
3902        window
3903            .update(cx, {
3904                let search_bar = search_bar.clone();
3905                let pane = first_pane.clone();
3906                move |workspace, window, cx| {
3907                    assert_eq!(workspace.panes().len(), 2);
3908                    pane.update(cx, move |pane, cx| {
3909                        pane.toolbar()
3910                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3911                    });
3912                }
3913            })
3914            .unwrap();
3915
3916        // Add a project search item to the second pane
3917        window
3918            .update(cx, {
3919                |workspace, window, cx| {
3920                    assert_eq!(workspace.panes().len(), 2);
3921                    second_pane.update(cx, |pane, cx| {
3922                        pane.toolbar()
3923                            .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
3924                    });
3925
3926                    ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
3927                }
3928            })
3929            .unwrap();
3930
3931        cx.run_until_parked();
3932        assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
3933        assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
3934
3935        // Focus the first pane
3936        window
3937            .update(cx, |workspace, window, cx| {
3938                assert_eq!(workspace.active_pane(), &second_pane);
3939                second_pane.update(cx, |this, cx| {
3940                    assert_eq!(this.active_item_index(), 1);
3941                    this.activate_previous_item(&Default::default(), window, cx);
3942                    assert_eq!(this.active_item_index(), 0);
3943                });
3944                workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx);
3945            })
3946            .unwrap();
3947        window
3948            .update(cx, |workspace, _, cx| {
3949                assert_eq!(workspace.active_pane(), &first_pane);
3950                assert_eq!(first_pane.read(cx).items_len(), 1);
3951                assert_eq!(second_pane.read(cx).items_len(), 2);
3952            })
3953            .unwrap();
3954
3955        // Deploy a new search
3956        cx.dispatch_action(window.into(), DeploySearch::find());
3957
3958        // Both panes should now have a project search in them
3959        window
3960            .update(cx, |workspace, window, cx| {
3961                assert_eq!(workspace.active_pane(), &first_pane);
3962                first_pane.read_with(cx, |this, _| {
3963                    assert_eq!(this.active_item_index(), 1);
3964                    assert_eq!(this.items_len(), 2);
3965                });
3966                second_pane.update(cx, |this, cx| {
3967                    assert!(!cx.focus_handle().contains_focused(window, cx));
3968                    assert_eq!(this.items_len(), 2);
3969                });
3970            })
3971            .unwrap();
3972
3973        // Focus the second pane's non-search item
3974        window
3975            .update(cx, |_workspace, window, cx| {
3976                second_pane.update(cx, |pane, cx| {
3977                    pane.activate_next_item(&Default::default(), window, cx)
3978                });
3979            })
3980            .unwrap();
3981
3982        // Deploy a new search
3983        cx.dispatch_action(window.into(), DeploySearch::find());
3984
3985        // The project search view should now be focused in the second pane
3986        // And the number of items should be unchanged.
3987        window
3988            .update(cx, |_workspace, _, cx| {
3989                second_pane.update(cx, |pane, _cx| {
3990                    assert!(
3991                        pane.active_item()
3992                            .unwrap()
3993                            .downcast::<ProjectSearchView>()
3994                            .is_some()
3995                    );
3996
3997                    assert_eq!(pane.items_len(), 2);
3998                });
3999            })
4000            .unwrap();
4001    }
4002
4003    #[perf]
4004    #[gpui::test]
4005    async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) {
4006        init_test(cx);
4007
4008        // We need many lines in the search results to be able to scroll the window
4009        let fs = FakeFs::new(cx.background_executor.clone());
4010        fs.insert_tree(
4011            path!("/dir"),
4012            json!({
4013                "1.txt": "\n\n\n\n\n A \n\n\n\n\n",
4014                "2.txt": "\n\n\n\n\n A \n\n\n\n\n",
4015                "3.rs": "\n\n\n\n\n A \n\n\n\n\n",
4016                "4.rs": "\n\n\n\n\n A \n\n\n\n\n",
4017                "5.rs": "\n\n\n\n\n A \n\n\n\n\n",
4018                "6.rs": "\n\n\n\n\n A \n\n\n\n\n",
4019                "7.rs": "\n\n\n\n\n A \n\n\n\n\n",
4020                "8.rs": "\n\n\n\n\n A \n\n\n\n\n",
4021                "9.rs": "\n\n\n\n\n A \n\n\n\n\n",
4022                "a.rs": "\n\n\n\n\n A \n\n\n\n\n",
4023                "b.rs": "\n\n\n\n\n B \n\n\n\n\n",
4024                "c.rs": "\n\n\n\n\n B \n\n\n\n\n",
4025                "d.rs": "\n\n\n\n\n B \n\n\n\n\n",
4026                "e.rs": "\n\n\n\n\n B \n\n\n\n\n",
4027                "f.rs": "\n\n\n\n\n B \n\n\n\n\n",
4028                "g.rs": "\n\n\n\n\n B \n\n\n\n\n",
4029                "h.rs": "\n\n\n\n\n B \n\n\n\n\n",
4030                "i.rs": "\n\n\n\n\n B \n\n\n\n\n",
4031                "j.rs": "\n\n\n\n\n B \n\n\n\n\n",
4032                "k.rs": "\n\n\n\n\n B \n\n\n\n\n",
4033            }),
4034        )
4035        .await;
4036        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4037        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4038        let workspace = window.root(cx).unwrap();
4039        let search = cx.new(|cx| ProjectSearch::new(project, cx));
4040        let search_view = cx.add_window(|window, cx| {
4041            ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
4042        });
4043
4044        // First search
4045        perform_search(search_view, "A", cx);
4046        search_view
4047            .update(cx, |search_view, window, cx| {
4048                search_view.results_editor.update(cx, |results_editor, cx| {
4049                    // Results are correct and scrolled to the top
4050                    assert_eq!(
4051                        results_editor.display_text(cx).match_indices(" A ").count(),
4052                        10
4053                    );
4054                    assert_eq!(results_editor.scroll_position(cx), Point::default());
4055
4056                    // Scroll results all the way down
4057                    results_editor.scroll(
4058                        Point::new(0., f64::MAX),
4059                        Some(Axis::Vertical),
4060                        window,
4061                        cx,
4062                    );
4063                });
4064            })
4065            .expect("unable to update search view");
4066
4067        // Second search
4068        perform_search(search_view, "B", cx);
4069        search_view
4070            .update(cx, |search_view, _, cx| {
4071                search_view.results_editor.update(cx, |results_editor, cx| {
4072                    // Results are correct...
4073                    assert_eq!(
4074                        results_editor.display_text(cx).match_indices(" B ").count(),
4075                        10
4076                    );
4077                    // ...and scrolled back to the top
4078                    assert_eq!(results_editor.scroll_position(cx), Point::default());
4079                });
4080            })
4081            .expect("unable to update search view");
4082    }
4083
4084    #[perf]
4085    #[gpui::test]
4086    async fn test_buffer_search_query_reused(cx: &mut TestAppContext) {
4087        init_test(cx);
4088
4089        let fs = FakeFs::new(cx.background_executor.clone());
4090        fs.insert_tree(
4091            path!("/dir"),
4092            json!({
4093                "one.rs": "const ONE: usize = 1;",
4094            }),
4095        )
4096        .await;
4097        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4098        let worktree_id = project.update(cx, |this, cx| {
4099            this.worktrees(cx).next().unwrap().read(cx).id()
4100        });
4101        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4102        let workspace = window.root(cx).unwrap();
4103        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
4104
4105        let editor = workspace
4106            .update_in(&mut cx, |workspace, window, cx| {
4107                workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
4108            })
4109            .await
4110            .unwrap()
4111            .downcast::<Editor>()
4112            .unwrap();
4113
4114        // Wait for the unstaged changes to be loaded
4115        cx.run_until_parked();
4116
4117        let buffer_search_bar = cx.new_window_entity(|window, cx| {
4118            let mut search_bar =
4119                BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx);
4120            search_bar.set_active_pane_item(Some(&editor), window, cx);
4121            search_bar.show(window, cx);
4122            search_bar
4123        });
4124
4125        let panes: Vec<_> = window
4126            .update(&mut cx, |this, _, _| this.panes().to_owned())
4127            .unwrap();
4128        assert_eq!(panes.len(), 1);
4129        let pane = panes.first().cloned().unwrap();
4130        pane.update_in(&mut cx, |pane, window, cx| {
4131            pane.toolbar().update(cx, |toolbar, cx| {
4132                toolbar.add_item(buffer_search_bar.clone(), window, cx);
4133            })
4134        });
4135
4136        let buffer_search_query = "search bar query";
4137        buffer_search_bar
4138            .update_in(&mut cx, |buffer_search_bar, window, cx| {
4139                buffer_search_bar.focus_handle(cx).focus(window);
4140                buffer_search_bar.search(buffer_search_query, None, true, window, cx)
4141            })
4142            .await
4143            .unwrap();
4144
4145        workspace.update_in(&mut cx, |workspace, window, cx| {
4146            ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
4147        });
4148        cx.run_until_parked();
4149        let project_search_view = pane
4150            .read_with(&cx, |pane, _| {
4151                pane.active_item()
4152                    .and_then(|item| item.downcast::<ProjectSearchView>())
4153            })
4154            .expect("should open a project search view after spawning a new search");
4155        project_search_view.update(&mut cx, |search_view, cx| {
4156            assert_eq!(
4157                search_view.search_query_text(cx),
4158                buffer_search_query,
4159                "Project search should take the query from the buffer search bar since it got focused and had a query inside"
4160            );
4161        });
4162    }
4163
4164    #[gpui::test]
4165    async fn test_search_dismisses_modal(cx: &mut TestAppContext) {
4166        init_test(cx);
4167
4168        let fs = FakeFs::new(cx.background_executor.clone());
4169        fs.insert_tree(
4170            path!("/dir"),
4171            json!({
4172                "one.rs": "const ONE: usize = 1;",
4173            }),
4174        )
4175        .await;
4176        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4177        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4178
4179        struct EmptyModalView {
4180            focus_handle: gpui::FocusHandle,
4181        }
4182        impl EventEmitter<gpui::DismissEvent> for EmptyModalView {}
4183        impl Render for EmptyModalView {
4184            fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
4185                div()
4186            }
4187        }
4188        impl Focusable for EmptyModalView {
4189            fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
4190                self.focus_handle.clone()
4191            }
4192        }
4193        impl workspace::ModalView for EmptyModalView {}
4194
4195        window
4196            .update(cx, |workspace, window, cx| {
4197                workspace.toggle_modal(window, cx, |_, cx| EmptyModalView {
4198                    focus_handle: cx.focus_handle(),
4199                });
4200                assert!(workspace.has_active_modal(window, cx));
4201            })
4202            .unwrap();
4203
4204        cx.dispatch_action(window.into(), Deploy::find());
4205
4206        window
4207            .update(cx, |workspace, window, cx| {
4208                assert!(!workspace.has_active_modal(window, cx));
4209                workspace.toggle_modal(window, cx, |_, cx| EmptyModalView {
4210                    focus_handle: cx.focus_handle(),
4211                });
4212                assert!(workspace.has_active_modal(window, cx));
4213            })
4214            .unwrap();
4215
4216        cx.dispatch_action(window.into(), DeploySearch::find());
4217
4218        window
4219            .update(cx, |workspace, window, cx| {
4220                assert!(!workspace.has_active_modal(window, cx));
4221            })
4222            .unwrap();
4223    }
4224
4225    fn init_test(cx: &mut TestAppContext) {
4226        cx.update(|cx| {
4227            let settings = SettingsStore::test(cx);
4228            cx.set_global(settings);
4229
4230            theme::init(theme::LoadThemes::JustBase, cx);
4231
4232            language::init(cx);
4233            client::init_settings(cx);
4234            editor::init(cx);
4235            workspace::init_settings(cx);
4236            Project::init_settings(cx);
4237            crate::init(cx);
4238        });
4239    }
4240
4241    fn perform_search(
4242        search_view: WindowHandle<ProjectSearchView>,
4243        text: impl Into<Arc<str>>,
4244        cx: &mut TestAppContext,
4245    ) {
4246        search_view
4247            .update(cx, |search_view, window, cx| {
4248                search_view.query_editor.update(cx, |query_editor, cx| {
4249                    query_editor.set_text(text, window, cx)
4250                });
4251                search_view.search(cx);
4252            })
4253            .unwrap();
4254        // Ensure editor highlights appear after the search is done
4255        cx.executor().advance_clock(
4256            editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100),
4257        );
4258        cx.background_executor.run_until_parked();
4259    }
4260}