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