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