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