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