project_search.rs

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