project_search.rs

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