project_search.rs

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