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