project_search.rs

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