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