project_search.rs

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