project_search.rs

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