project_search.rs

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