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