project_search.rs

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