project_search.rs

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