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