project_search.rs

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