project_search.rs

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