project_search.rs

   1use crate::{
   2    history::SearchHistory,
   3    mode::SearchMode,
   4    search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
   5    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch,
   6    SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
   7};
   8use anyhow::Context;
   9use collections::HashMap;
  10use editor::{
  11    items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
  12    SelectAll, MAX_TAB_TITLE_LEN,
  13};
  14use futures::StreamExt;
  15
  16use gpui::{
  17    actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext,
  18    Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
  19    WeakModelHandle, WeakViewHandle,
  20};
  21
  22use menu::Confirm;
  23use project::{
  24    search::{PathMatcher, SearchQuery},
  25    Entry, Project,
  26};
  27use smallvec::SmallVec;
  28use std::{
  29    any::{Any, TypeId},
  30    borrow::Cow,
  31    collections::HashSet,
  32    mem,
  33    ops::{Not, Range},
  34    path::PathBuf,
  35    sync::Arc,
  36};
  37use util::ResultExt as _;
  38use workspace::{
  39    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
  40    searchable::{Direction, SearchableItem, SearchableItemHandle},
  41    ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
  42};
  43
  44actions!(
  45    project_search,
  46    [SearchInNew, ToggleFocus, NextField, ToggleFilters,]
  47);
  48
  49#[derive(Default)]
  50struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
  51
  52pub fn init(cx: &mut AppContext) {
  53    cx.set_global(ActiveSearches::default());
  54    cx.add_action(ProjectSearchView::deploy);
  55    cx.add_action(ProjectSearchView::move_focus_to_results);
  56    cx.add_action(ProjectSearchBar::search);
  57    cx.add_action(ProjectSearchBar::search_in_new);
  58    cx.add_action(ProjectSearchBar::select_next_match);
  59    cx.add_action(ProjectSearchBar::select_prev_match);
  60    cx.add_action(ProjectSearchBar::cycle_mode);
  61    cx.add_action(ProjectSearchBar::next_history_query);
  62    cx.add_action(ProjectSearchBar::previous_history_query);
  63    // cx.add_action(ProjectSearchBar::activate_regex_mode);
  64    cx.capture_action(ProjectSearchBar::tab);
  65    cx.capture_action(ProjectSearchBar::tab_previous);
  66    add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
  67    add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
  68    add_toggle_filters_action::<ToggleFilters>(cx);
  69}
  70
  71fn add_toggle_filters_action<A: Action>(cx: &mut AppContext) {
  72    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  73        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
  74            if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) {
  75                return;
  76            }
  77        }
  78        cx.propagate_action();
  79    });
  80}
  81
  82fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
  83    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
  84        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
  85            if search_bar.update(cx, |search_bar, cx| {
  86                search_bar.toggle_search_option(option, cx)
  87            }) {
  88                return;
  89            }
  90        }
  91        cx.propagate_action();
  92    });
  93}
  94
  95struct ProjectSearch {
  96    project: ModelHandle<Project>,
  97    excerpts: ModelHandle<MultiBuffer>,
  98    pending_search: Option<Task<Option<()>>>,
  99    match_ranges: Vec<Range<Anchor>>,
 100    active_query: Option<SearchQuery>,
 101    search_id: usize,
 102    search_history: SearchHistory,
 103    no_results: Option<bool>,
 104}
 105
 106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 107enum InputPanel {
 108    Query,
 109    Exclude,
 110    Include,
 111}
 112
 113pub struct ProjectSearchView {
 114    model: ModelHandle<ProjectSearch>,
 115    query_editor: ViewHandle<Editor>,
 116    results_editor: ViewHandle<Editor>,
 117    search_options: SearchOptions,
 118    panels_with_errors: HashSet<InputPanel>,
 119    active_match_index: Option<usize>,
 120    search_id: usize,
 121    query_editor_was_focused: bool,
 122    included_files_editor: ViewHandle<Editor>,
 123    excluded_files_editor: ViewHandle<Editor>,
 124    filters_enabled: bool,
 125    current_mode: SearchMode,
 126}
 127
 128pub struct ProjectSearchBar {
 129    active_project_search: Option<ViewHandle<ProjectSearchView>>,
 130    subscription: Option<Subscription>,
 131}
 132
 133impl Entity for ProjectSearch {
 134    type Event = ();
 135}
 136
 137impl ProjectSearch {
 138    fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
 139        let replica_id = project.read(cx).replica_id();
 140        Self {
 141            project,
 142            excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
 143            pending_search: Default::default(),
 144            match_ranges: Default::default(),
 145            active_query: None,
 146            search_id: 0,
 147            search_history: SearchHistory::default(),
 148            no_results: None,
 149        }
 150    }
 151
 152    fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
 153        cx.add_model(|cx| Self {
 154            project: self.project.clone(),
 155            excerpts: self
 156                .excerpts
 157                .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
 158            pending_search: Default::default(),
 159            match_ranges: self.match_ranges.clone(),
 160            active_query: self.active_query.clone(),
 161            search_id: self.search_id,
 162            search_history: self.search_history.clone(),
 163            no_results: self.no_results.clone(),
 164        })
 165    }
 166
 167    fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
 168        let search = self
 169            .project
 170            .update(cx, |project, cx| project.search(query.clone(), cx));
 171        self.search_id += 1;
 172        self.search_history.add(query.as_str().to_string());
 173        self.active_query = Some(query);
 174        self.match_ranges.clear();
 175        self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
 176            let matches = search.await.log_err()?;
 177            let this = this.upgrade(&cx)?;
 178            let mut matches = matches.into_iter().collect::<Vec<_>>();
 179            let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
 180                this.match_ranges.clear();
 181                this.no_results = Some(true);
 182                matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
 183                this.excerpts.update(cx, |excerpts, cx| {
 184                    excerpts.clear(cx);
 185                    excerpts.stream_excerpts_with_context_lines(matches, 1, cx)
 186                })
 187            });
 188
 189            while let Some(match_range) = match_ranges.next().await {
 190                this.update(&mut cx, |this, cx| {
 191                    this.match_ranges.push(match_range);
 192                    while let Ok(Some(match_range)) = match_ranges.try_next() {
 193                        this.match_ranges.push(match_range);
 194                    }
 195                    this.no_results = Some(false);
 196                    cx.notify();
 197                });
 198            }
 199
 200            this.update(&mut cx, |this, cx| {
 201                this.pending_search.take();
 202                cx.notify();
 203            });
 204
 205            None
 206        }));
 207        cx.notify();
 208    }
 209}
 210
 211#[derive(Clone, Debug, PartialEq, Eq)]
 212pub enum ViewEvent {
 213    UpdateTab,
 214    Activate,
 215    EditorEvent(editor::Event),
 216    Dismiss,
 217}
 218
 219impl Entity for ProjectSearchView {
 220    type Event = ViewEvent;
 221}
 222
 223impl View for ProjectSearchView {
 224    fn ui_name() -> &'static str {
 225        "ProjectSearchView"
 226    }
 227
 228    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 229        let model = &self.model.read(cx);
 230        if model.match_ranges.is_empty() {
 231            enum Status {}
 232
 233            let theme = theme::current(cx).clone();
 234
 235            // If Search is Active -> Major: Searching..., Minor: None
 236            // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
 237            // If Regex -> Major: "Search using Regex", Minor: {ex...}
 238            // If Text -> Major: "Text search all files and folders", Minor: {...}
 239
 240            let current_mode = self.current_mode;
 241            let major_text = if model.pending_search.is_some() {
 242                Cow::Borrowed("Searching...")
 243            } else if model.no_results.is_some_and(|v| v) {
 244                Cow::Borrowed("No Results")
 245            } else {
 246                match current_mode {
 247                    SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
 248                    SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
 249                }
 250            };
 251
 252            let minor_text = if let Some(no_results) = model.no_results {
 253                if model.pending_search.is_none() && no_results {
 254                    vec!["No results found in this project for the provided query".to_owned()]
 255                } else {
 256                    vec![]
 257                }
 258            } else {
 259                vec![
 260                    "".to_owned(),
 261                    "Include/exclude specific paths with the filter option.".to_owned(),
 262                    "Matching exact word and/or casing is available too.".to_owned(),
 263                ]
 264            };
 265
 266            let previous_query_keystrokes =
 267                cx.binding_for_action(&PreviousHistoryQuery {})
 268                    .map(|binding| {
 269                        binding
 270                            .keystrokes()
 271                            .iter()
 272                            .map(|k| k.to_string())
 273                            .collect::<Vec<_>>()
 274                    });
 275            let next_query_keystrokes =
 276                cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
 277                    binding
 278                        .keystrokes()
 279                        .iter()
 280                        .map(|k| k.to_string())
 281                        .collect::<Vec<_>>()
 282                });
 283            let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
 284                (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
 285                    format!(
 286                        "Search ({}/{} for previous/next query)",
 287                        previous_query_keystrokes.join(" "),
 288                        next_query_keystrokes.join(" ")
 289                    )
 290                }
 291                (None, Some(next_query_keystrokes)) => {
 292                    format!(
 293                        "Search ({} for next query)",
 294                        next_query_keystrokes.join(" ")
 295                    )
 296                }
 297                (Some(previous_query_keystrokes), None) => {
 298                    format!(
 299                        "Search ({} for previous query)",
 300                        previous_query_keystrokes.join(" ")
 301                    )
 302                }
 303                (None, None) => String::new(),
 304            };
 305            self.query_editor.update(cx, |editor, cx| {
 306                editor.set_placeholder_text(new_placeholder_text, cx);
 307            });
 308
 309            MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
 310                Flex::column()
 311                    .with_child(Flex::column().contained().flex(1., true))
 312                    .with_child(
 313                        Flex::column()
 314                            .align_children_center()
 315                            .with_child(Label::new(
 316                                major_text,
 317                                theme.search.major_results_status.clone(),
 318                            ))
 319                            .with_children(
 320                                minor_text.into_iter().map(|x| {
 321                                    Label::new(x, theme.search.minor_results_status.clone())
 322                                }),
 323                            )
 324                            .aligned()
 325                            .top()
 326                            .contained()
 327                            .flex(7., true),
 328                    )
 329                    .contained()
 330                    .with_background_color(theme.editor.background)
 331            })
 332            .on_down(MouseButton::Left, |_, _, cx| {
 333                cx.focus_parent();
 334            })
 335            .into_any_named("project search view")
 336        } else {
 337            ChildView::new(&self.results_editor, cx)
 338                .flex(1., true)
 339                .into_any_named("project search view")
 340        }
 341    }
 342
 343    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 344        let handle = cx.weak_handle();
 345        cx.update_global(|state: &mut ActiveSearches, cx| {
 346            state
 347                .0
 348                .insert(self.model.read(cx).project.downgrade(), handle)
 349        });
 350
 351        if cx.is_self_focused() {
 352            if self.query_editor_was_focused {
 353                cx.focus(&self.query_editor);
 354            } else {
 355                cx.focus(&self.results_editor);
 356            }
 357        }
 358    }
 359}
 360
 361impl Item for ProjectSearchView {
 362    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
 363        let query_text = self.query_editor.read(cx).text(cx);
 364
 365        query_text
 366            .is_empty()
 367            .not()
 368            .then(|| query_text.into())
 369            .or_else(|| Some("Project Search".into()))
 370    }
 371    fn should_close_item_on_event(event: &Self::Event) -> bool {
 372        event == &Self::Event::Dismiss
 373    }
 374    fn act_as_type<'a>(
 375        &'a self,
 376        type_id: TypeId,
 377        self_handle: &'a ViewHandle<Self>,
 378        _: &'a AppContext,
 379    ) -> Option<&'a AnyViewHandle> {
 380        if type_id == TypeId::of::<Self>() {
 381            Some(self_handle)
 382        } else if type_id == TypeId::of::<Editor>() {
 383            Some(&self.results_editor)
 384        } else {
 385            None
 386        }
 387    }
 388
 389    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 390        self.results_editor
 391            .update(cx, |editor, cx| editor.deactivated(cx));
 392    }
 393
 394    fn tab_content<T: View>(
 395        &self,
 396        _detail: Option<usize>,
 397        tab_theme: &theme::Tab,
 398        cx: &AppContext,
 399    ) -> AnyElement<T> {
 400        Flex::row()
 401            .with_child(
 402                Svg::new("icons/magnifying_glass_12.svg")
 403                    .with_color(tab_theme.label.text.color)
 404                    .constrained()
 405                    .with_width(tab_theme.type_icon_width)
 406                    .aligned()
 407                    .contained()
 408                    .with_margin_right(tab_theme.spacing),
 409            )
 410            .with_child({
 411                let tab_name: Option<Cow<_>> =
 412                    self.model.read(cx).active_query.as_ref().map(|query| {
 413                        let query_text =
 414                            util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN);
 415                        query_text.into()
 416                    });
 417                Label::new(
 418                    tab_name
 419                        .filter(|name| !name.is_empty())
 420                        .unwrap_or("Project search".into()),
 421                    tab_theme.label.clone(),
 422                )
 423                .aligned()
 424            })
 425            .into_any()
 426    }
 427
 428    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 429        self.results_editor.for_each_project_item(cx, f)
 430    }
 431
 432    fn is_singleton(&self, _: &AppContext) -> bool {
 433        false
 434    }
 435
 436    fn can_save(&self, _: &AppContext) -> bool {
 437        true
 438    }
 439
 440    fn is_dirty(&self, cx: &AppContext) -> bool {
 441        self.results_editor.read(cx).is_dirty(cx)
 442    }
 443
 444    fn has_conflict(&self, cx: &AppContext) -> bool {
 445        self.results_editor.read(cx).has_conflict(cx)
 446    }
 447
 448    fn save(
 449        &mut self,
 450        project: ModelHandle<Project>,
 451        cx: &mut ViewContext<Self>,
 452    ) -> Task<anyhow::Result<()>> {
 453        self.results_editor
 454            .update(cx, |editor, cx| editor.save(project, cx))
 455    }
 456
 457    fn save_as(
 458        &mut self,
 459        _: ModelHandle<Project>,
 460        _: PathBuf,
 461        _: &mut ViewContext<Self>,
 462    ) -> Task<anyhow::Result<()>> {
 463        unreachable!("save_as should not have been called")
 464    }
 465
 466    fn reload(
 467        &mut self,
 468        project: ModelHandle<Project>,
 469        cx: &mut ViewContext<Self>,
 470    ) -> Task<anyhow::Result<()>> {
 471        self.results_editor
 472            .update(cx, |editor, cx| editor.reload(project, cx))
 473    }
 474
 475    fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
 476    where
 477        Self: Sized,
 478    {
 479        let model = self.model.update(cx, |model, cx| model.clone(cx));
 480        Some(Self::new(model, cx))
 481    }
 482
 483    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 484        self.results_editor
 485            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 486    }
 487
 488    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 489        self.results_editor.update(cx, |editor, _| {
 490            editor.set_nav_history(Some(nav_history));
 491        });
 492    }
 493
 494    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 495        self.results_editor
 496            .update(cx, |editor, cx| editor.navigate(data, cx))
 497    }
 498
 499    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 500        match event {
 501            ViewEvent::UpdateTab => {
 502                smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
 503            }
 504            ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
 505            ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem],
 506            _ => SmallVec::new(),
 507        }
 508    }
 509
 510    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 511        if self.has_matches() {
 512            ToolbarItemLocation::Secondary
 513        } else {
 514            ToolbarItemLocation::Hidden
 515        }
 516    }
 517
 518    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 519        self.results_editor.breadcrumbs(theme, cx)
 520    }
 521
 522    fn serialized_item_kind() -> Option<&'static str> {
 523        None
 524    }
 525
 526    fn deserialize(
 527        _project: ModelHandle<Project>,
 528        _workspace: WeakViewHandle<Workspace>,
 529        _workspace_id: workspace::WorkspaceId,
 530        _item_id: workspace::ItemId,
 531        _cx: &mut ViewContext<Pane>,
 532    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
 533        unimplemented!()
 534    }
 535}
 536
 537impl ProjectSearchView {
 538    fn toggle_search_option(&mut self, option: SearchOptions) {
 539        self.search_options.toggle(option);
 540    }
 541
 542    fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
 543        self.model.update(cx, |model, cx| {
 544            model.pending_search = None;
 545            model.no_results = None;
 546            model.match_ranges.clear();
 547
 548            model.excerpts.update(cx, |excerpts, cx| {
 549                excerpts.clear(cx);
 550            });
 551        });
 552    }
 553
 554    fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
 555        let previous_mode = self.current_mode;
 556        if previous_mode == mode {
 557            return;
 558        }
 559
 560        self.clear_search(cx);
 561        self.current_mode = mode;
 562        self.active_match_index = None;
 563
 564        self.search(cx);
 565
 566        cx.notify();
 567    }
 568
 569    fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
 570        let project;
 571        let excerpts;
 572        let mut query_text = String::new();
 573        let mut options = SearchOptions::NONE;
 574
 575        {
 576            let model = model.read(cx);
 577            project = model.project.clone();
 578            excerpts = model.excerpts.clone();
 579            if let Some(active_query) = model.active_query.as_ref() {
 580                query_text = active_query.as_str().to_string();
 581                options = SearchOptions::from_query(active_query);
 582            }
 583        }
 584        cx.observe(&model, |this, _, cx| this.model_changed(cx))
 585            .detach();
 586
 587        let query_editor = cx.add_view(|cx| {
 588            let mut editor = Editor::single_line(
 589                Some(Arc::new(|theme| theme.search.editor.input.clone())),
 590                cx,
 591            );
 592            editor.set_placeholder_text("Text search all files", cx);
 593            editor.set_text(query_text, cx);
 594            editor
 595        });
 596        // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
 597        cx.subscribe(&query_editor, |_, _, event, cx| {
 598            cx.emit(ViewEvent::EditorEvent(event.clone()))
 599        })
 600        .detach();
 601
 602        let results_editor = cx.add_view(|cx| {
 603            let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
 604            editor.set_searchable(false);
 605            editor
 606        });
 607        cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
 608            .detach();
 609
 610        cx.subscribe(&results_editor, |this, _, event, cx| {
 611            if matches!(event, editor::Event::SelectionsChanged { .. }) {
 612                this.update_match_index(cx);
 613            }
 614            // Reraise editor events for workspace item activation purposes
 615            cx.emit(ViewEvent::EditorEvent(event.clone()));
 616        })
 617        .detach();
 618
 619        let included_files_editor = cx.add_view(|cx| {
 620            let mut editor = Editor::single_line(
 621                Some(Arc::new(|theme| {
 622                    theme.search.include_exclude_editor.input.clone()
 623                })),
 624                cx,
 625            );
 626            editor.set_placeholder_text("Include: crates/**/*.toml", cx);
 627
 628            editor
 629        });
 630        // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
 631        cx.subscribe(&included_files_editor, |_, _, event, cx| {
 632            cx.emit(ViewEvent::EditorEvent(event.clone()))
 633        })
 634        .detach();
 635
 636        let excluded_files_editor = cx.add_view(|cx| {
 637            let mut editor = Editor::single_line(
 638                Some(Arc::new(|theme| {
 639                    theme.search.include_exclude_editor.input.clone()
 640                })),
 641                cx,
 642            );
 643            editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
 644
 645            editor
 646        });
 647        // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
 648        cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
 649            cx.emit(ViewEvent::EditorEvent(event.clone()))
 650        })
 651        .detach();
 652        let filters_enabled = false;
 653
 654        // Check if Worktrees have all been previously indexed
 655        let mut this = ProjectSearchView {
 656            search_id: model.read(cx).search_id,
 657            model,
 658            query_editor,
 659            results_editor,
 660            search_options: options,
 661            panels_with_errors: HashSet::new(),
 662            active_match_index: None,
 663            query_editor_was_focused: false,
 664            included_files_editor,
 665            excluded_files_editor,
 666            filters_enabled,
 667            current_mode: Default::default(),
 668        };
 669        this.model_changed(cx);
 670        this
 671    }
 672
 673    pub fn new_search_in_directory(
 674        workspace: &mut Workspace,
 675        dir_entry: &Entry,
 676        cx: &mut ViewContext<Workspace>,
 677    ) {
 678        if !dir_entry.is_dir() {
 679            return;
 680        }
 681        let Some(filter_str) = dir_entry.path.to_str() else { return; };
 682
 683        let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
 684        let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
 685        workspace.add_item(Box::new(search.clone()), cx);
 686        search.update(cx, |search, cx| {
 687            search
 688                .included_files_editor
 689                .update(cx, |editor, cx| editor.set_text(filter_str, cx));
 690            search.focus_query_editor(cx)
 691        });
 692    }
 693
 694    // Re-activate the most recently activated search or the most recent if it has been closed.
 695    // If no search exists in the workspace, create a new one.
 696    fn deploy(
 697        workspace: &mut Workspace,
 698        _: &workspace::NewSearch,
 699        cx: &mut ViewContext<Workspace>,
 700    ) {
 701        // Clean up entries for dropped projects
 702        cx.update_global(|state: &mut ActiveSearches, cx| {
 703            state.0.retain(|project, _| project.is_upgradable(cx))
 704        });
 705
 706        let active_search = cx
 707            .global::<ActiveSearches>()
 708            .0
 709            .get(&workspace.project().downgrade());
 710
 711        let existing = active_search
 712            .and_then(|active_search| {
 713                workspace
 714                    .items_of_type::<ProjectSearchView>(cx)
 715                    .find(|search| search == active_search)
 716            })
 717            .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
 718
 719        let query = workspace.active_item(cx).and_then(|item| {
 720            let editor = item.act_as::<Editor>(cx)?;
 721            let query = editor.query_suggestion(cx);
 722            if query.is_empty() {
 723                None
 724            } else {
 725                Some(query)
 726            }
 727        });
 728
 729        let search = if let Some(existing) = existing {
 730            workspace.activate_item(&existing, cx);
 731            existing
 732        } else {
 733            let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
 734            let view = cx.add_view(|cx| ProjectSearchView::new(model, cx));
 735            workspace.add_item(Box::new(view.clone()), cx);
 736            view
 737        };
 738
 739        search.update(cx, |search, cx| {
 740            if let Some(query) = query {
 741                search.set_query(&query, cx);
 742            }
 743            search.focus_query_editor(cx)
 744        });
 745    }
 746
 747    fn search(&mut self, cx: &mut ViewContext<Self>) {
 748        if let Some(query) = self.build_search_query(cx) {
 749            self.model.update(cx, |model, cx| model.search(query, cx));
 750        }
 751    }
 752
 753    fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
 754        let text = self.query_editor.read(cx).text(cx);
 755        let included_files =
 756            match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
 757                Ok(included_files) => {
 758                    self.panels_with_errors.remove(&InputPanel::Include);
 759                    included_files
 760                }
 761                Err(_e) => {
 762                    self.panels_with_errors.insert(InputPanel::Include);
 763                    cx.notify();
 764                    return None;
 765                }
 766            };
 767        let excluded_files =
 768            match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
 769                Ok(excluded_files) => {
 770                    self.panels_with_errors.remove(&InputPanel::Exclude);
 771                    excluded_files
 772                }
 773                Err(_e) => {
 774                    self.panels_with_errors.insert(InputPanel::Exclude);
 775                    cx.notify();
 776                    return None;
 777                }
 778            };
 779        let current_mode = self.current_mode;
 780        match current_mode {
 781            SearchMode::Regex => {
 782                match SearchQuery::regex(
 783                    text,
 784                    self.search_options.contains(SearchOptions::WHOLE_WORD),
 785                    self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 786                    included_files,
 787                    excluded_files,
 788                ) {
 789                    Ok(query) => {
 790                        self.panels_with_errors.remove(&InputPanel::Query);
 791                        Some(query)
 792                    }
 793                    Err(_e) => {
 794                        self.panels_with_errors.insert(InputPanel::Query);
 795                        cx.notify();
 796                        None
 797                    }
 798                }
 799            }
 800            _ => Some(SearchQuery::text(
 801                text,
 802                self.search_options.contains(SearchOptions::WHOLE_WORD),
 803                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
 804                included_files,
 805                excluded_files,
 806            )),
 807        }
 808    }
 809
 810    fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
 811        text.split(',')
 812            .map(str::trim)
 813            .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
 814            .map(|maybe_glob_str| {
 815                PathMatcher::new(maybe_glob_str)
 816                    .with_context(|| format!("parsing {maybe_glob_str} as path matcher"))
 817            })
 818            .collect()
 819    }
 820
 821    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
 822        if let Some(index) = self.active_match_index {
 823            let match_ranges = self.model.read(cx).match_ranges.clone();
 824            let new_index = self.results_editor.update(cx, |editor, cx| {
 825                editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
 826            });
 827
 828            let range_to_select = match_ranges[new_index].clone();
 829            self.results_editor.update(cx, |editor, cx| {
 830                let range_to_select = editor.range_for_match(&range_to_select);
 831                editor.unfold_ranges([range_to_select.clone()], false, true, cx);
 832                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 833                    s.select_ranges([range_to_select])
 834                });
 835            });
 836        }
 837    }
 838
 839    fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
 840        self.query_editor.update(cx, |query_editor, cx| {
 841            query_editor.select_all(&SelectAll, cx);
 842        });
 843        self.query_editor_was_focused = true;
 844        cx.focus(&self.query_editor);
 845    }
 846
 847    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
 848        self.query_editor
 849            .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
 850    }
 851
 852    fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
 853        self.query_editor.update(cx, |query_editor, cx| {
 854            let cursor = query_editor.selections.newest_anchor().head();
 855            query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
 856        });
 857        self.query_editor_was_focused = false;
 858        cx.focus(&self.results_editor);
 859    }
 860
 861    fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
 862        let match_ranges = self.model.read(cx).match_ranges.clone();
 863        if match_ranges.is_empty() {
 864            self.active_match_index = None;
 865        } else {
 866            self.active_match_index = Some(0);
 867            self.update_match_index(cx);
 868            let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
 869            let is_new_search = self.search_id != prev_search_id;
 870            self.results_editor.update(cx, |editor, cx| {
 871                if is_new_search {
 872                    let range_to_select = match_ranges
 873                        .first()
 874                        .clone()
 875                        .map(|range| editor.range_for_match(range));
 876                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 877                        s.select_ranges(range_to_select)
 878                    });
 879                }
 880                editor.highlight_background::<Self>(
 881                    match_ranges,
 882                    |theme| theme.search.match_background,
 883                    cx,
 884                );
 885            });
 886            if is_new_search && self.query_editor.is_focused(cx) {
 887                self.focus_results_editor(cx);
 888            }
 889        }
 890
 891        cx.emit(ViewEvent::UpdateTab);
 892        cx.notify();
 893    }
 894
 895    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
 896        let results_editor = self.results_editor.read(cx);
 897        let new_index = active_match_index(
 898            &self.model.read(cx).match_ranges,
 899            &results_editor.selections.newest_anchor().head(),
 900            &results_editor.buffer().read(cx).snapshot(cx),
 901        );
 902        if self.active_match_index != new_index {
 903            self.active_match_index = new_index;
 904            cx.notify();
 905        }
 906    }
 907
 908    pub fn has_matches(&self) -> bool {
 909        self.active_match_index.is_some()
 910    }
 911
 912    fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
 913        if let Some(search_view) = pane
 914            .active_item()
 915            .and_then(|item| item.downcast::<ProjectSearchView>())
 916        {
 917            search_view.update(cx, |search_view, cx| {
 918                if !search_view.results_editor.is_focused(cx)
 919                    && !search_view.model.read(cx).match_ranges.is_empty()
 920                {
 921                    return search_view.focus_results_editor(cx);
 922                }
 923            });
 924        }
 925
 926        cx.propagate_action();
 927    }
 928}
 929
 930impl Default for ProjectSearchBar {
 931    fn default() -> Self {
 932        Self::new()
 933    }
 934}
 935
 936impl ProjectSearchBar {
 937    pub fn new() -> Self {
 938        Self {
 939            active_project_search: Default::default(),
 940            subscription: Default::default(),
 941        }
 942    }
 943    fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext<Workspace>) {
 944        if let Some(search_view) = workspace
 945            .active_item(cx)
 946            .and_then(|item| item.downcast::<ProjectSearchView>())
 947        {
 948            search_view.update(cx, |this, cx| {
 949                let new_mode = crate::mode::next_mode(&this.current_mode);
 950                this.activate_search_mode(new_mode, cx);
 951                cx.focus(&this.query_editor);
 952            })
 953        }
 954    }
 955    fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
 956        if let Some(search_view) = self.active_project_search.as_ref() {
 957            search_view.update(cx, |search_view, cx| search_view.search(cx));
 958        }
 959    }
 960
 961    fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
 962        if let Some(search_view) = workspace
 963            .active_item(cx)
 964            .and_then(|item| item.downcast::<ProjectSearchView>())
 965        {
 966            let new_query = search_view.update(cx, |search_view, cx| {
 967                let new_query = search_view.build_search_query(cx);
 968                if new_query.is_some() {
 969                    if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
 970                        search_view.query_editor.update(cx, |editor, cx| {
 971                            editor.set_text(old_query.as_str(), cx);
 972                        });
 973                        search_view.search_options = SearchOptions::from_query(&old_query);
 974                    }
 975                }
 976                new_query
 977            });
 978            if let Some(new_query) = new_query {
 979                let model = cx.add_model(|cx| {
 980                    let mut model = ProjectSearch::new(workspace.project().clone(), cx);
 981                    model.search(new_query, cx);
 982                    model
 983                });
 984                workspace.add_item(
 985                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
 986                    cx,
 987                );
 988            }
 989        }
 990    }
 991
 992    fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
 993        if let Some(search_view) = pane
 994            .active_item()
 995            .and_then(|item| item.downcast::<ProjectSearchView>())
 996        {
 997            search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
 998        } else {
 999            cx.propagate_action();
1000        }
1001    }
1002
1003    fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
1004        if let Some(search_view) = pane
1005            .active_item()
1006            .and_then(|item| item.downcast::<ProjectSearchView>())
1007        {
1008            search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
1009        } else {
1010            cx.propagate_action();
1011        }
1012    }
1013
1014    fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
1015        self.cycle_field(Direction::Next, cx);
1016    }
1017
1018    fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
1019        self.cycle_field(Direction::Prev, cx);
1020    }
1021
1022    fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
1023        let active_project_search = match &self.active_project_search {
1024            Some(active_project_search) => active_project_search,
1025
1026            None => {
1027                cx.propagate_action();
1028                return;
1029            }
1030        };
1031
1032        active_project_search.update(cx, |project_view, cx| {
1033            let views = &[
1034                &project_view.query_editor,
1035                &project_view.included_files_editor,
1036                &project_view.excluded_files_editor,
1037            ];
1038
1039            let current_index = match views
1040                .iter()
1041                .enumerate()
1042                .find(|(_, view)| view.is_focused(cx))
1043            {
1044                Some((index, _)) => index,
1045
1046                None => {
1047                    cx.propagate_action();
1048                    return;
1049                }
1050            };
1051
1052            let new_index = match direction {
1053                Direction::Next => (current_index + 1) % views.len(),
1054                Direction::Prev if current_index == 0 => views.len() - 1,
1055                Direction::Prev => (current_index - 1) % views.len(),
1056            };
1057            cx.focus(views[new_index]);
1058        });
1059    }
1060
1061    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
1062        if let Some(search_view) = self.active_project_search.as_ref() {
1063            search_view.update(cx, |search_view, cx| {
1064                search_view.toggle_search_option(option);
1065                search_view.search(cx);
1066            });
1067            cx.notify();
1068            true
1069        } else {
1070            false
1071        }
1072    }
1073
1074    // fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
1075    //     if let Some(search_view) = pane
1076    //         .active_item()
1077    //         .and_then(|item| item.downcast::<ProjectSearchView>())
1078    //     {
1079    //         search_view.update(cx, |view, cx| {
1080    //             view.activate_search_mode(SearchMode::Regex, cx)
1081    //         });
1082    //     } else {
1083    //         cx.propagate_action();
1084    //     }
1085    // }
1086
1087    fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
1088        if let Some(search_view) = self.active_project_search.as_ref() {
1089            search_view.update(cx, |search_view, cx| {
1090                search_view.filters_enabled = !search_view.filters_enabled;
1091                search_view
1092                    .included_files_editor
1093                    .update(cx, |_, cx| cx.notify());
1094                search_view
1095                    .excluded_files_editor
1096                    .update(cx, |_, cx| cx.notify());
1097                cx.refresh_windows();
1098                cx.notify();
1099            });
1100            cx.notify();
1101            true
1102        } else {
1103            false
1104        }
1105    }
1106
1107    fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
1108        // Update Current Mode
1109        if let Some(search_view) = self.active_project_search.as_ref() {
1110            search_view.update(cx, |search_view, cx| {
1111                search_view.activate_search_mode(mode, cx);
1112            });
1113            cx.notify();
1114        }
1115    }
1116
1117    fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
1118        if let Some(search) = self.active_project_search.as_ref() {
1119            search.read(cx).search_options.contains(option)
1120        } else {
1121            false
1122        }
1123    }
1124
1125    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
1126        if let Some(search_view) = self.active_project_search.as_ref() {
1127            search_view.update(cx, |search_view, cx| {
1128                let new_query = search_view.model.update(cx, |model, _| {
1129                    if let Some(new_query) = model.search_history.next().map(str::to_string) {
1130                        new_query
1131                    } else {
1132                        model.search_history.reset_selection();
1133                        String::new()
1134                    }
1135                });
1136                search_view.set_query(&new_query, cx);
1137            });
1138        }
1139    }
1140
1141    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
1142        if let Some(search_view) = self.active_project_search.as_ref() {
1143            search_view.update(cx, |search_view, cx| {
1144                if search_view.query_editor.read(cx).text(cx).is_empty() {
1145                    if let Some(new_query) = search_view
1146                        .model
1147                        .read(cx)
1148                        .search_history
1149                        .current()
1150                        .map(str::to_string)
1151                    {
1152                        search_view.set_query(&new_query, cx);
1153                        return;
1154                    }
1155                }
1156
1157                if let Some(new_query) = search_view.model.update(cx, |model, _| {
1158                    model.search_history.previous().map(str::to_string)
1159                }) {
1160                    search_view.set_query(&new_query, cx);
1161                }
1162            });
1163        }
1164    }
1165}
1166
1167impl Entity for ProjectSearchBar {
1168    type Event = ();
1169}
1170
1171impl View for ProjectSearchBar {
1172    fn ui_name() -> &'static str {
1173        "ProjectSearchBar"
1174    }
1175
1176    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1177        if let Some(_search) = self.active_project_search.as_ref() {
1178            let search = _search.read(cx);
1179            let theme = theme::current(cx).clone();
1180            let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
1181                theme.search.invalid_editor
1182            } else {
1183                theme.search.editor.input.container
1184            };
1185
1186            let search = _search.read(cx);
1187            let filter_button = render_option_button_icon(
1188                search.filters_enabled,
1189                "icons/filter_12.svg",
1190                0,
1191                "Toggle filters",
1192                Box::new(ToggleFilters),
1193                move |_, this, cx| {
1194                    this.toggle_filters(cx);
1195                },
1196                cx,
1197            );
1198
1199            let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
1200                crate::search_bar::render_option_button_icon(
1201                    self.is_option_enabled(option, cx),
1202                    path,
1203                    option.bits as usize,
1204                    format!("Toggle {}", option.label()),
1205                    option.to_toggle_action(),
1206                    move |_, this, cx| {
1207                        this.toggle_search_option(option, cx);
1208                    },
1209                    cx,
1210                )
1211            };
1212            let case_sensitive = render_option_button_icon(
1213                "icons/case_insensitive_12.svg",
1214                SearchOptions::CASE_SENSITIVE,
1215                cx,
1216            );
1217
1218            let whole_word = render_option_button_icon(
1219                "icons/word_search_12.svg",
1220                SearchOptions::WHOLE_WORD,
1221                cx,
1222            );
1223
1224            let search = _search.read(cx);
1225            let icon_style = theme.search.editor_icon.clone();
1226
1227            // Editor Functionality
1228            let query = Flex::row()
1229                .with_child(
1230                    Svg::for_style(icon_style.icon)
1231                        .contained()
1232                        .with_style(icon_style.container),
1233                )
1234                .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
1235                .with_child(
1236                    Flex::row()
1237                        .with_child(filter_button)
1238                        .with_child(case_sensitive)
1239                        .with_child(whole_word)
1240                        .flex(1., false)
1241                        .constrained()
1242                        .contained(),
1243                )
1244                .align_children_center()
1245                .flex(1., true);
1246
1247            let search = _search.read(cx);
1248
1249            let include_container_style =
1250                if search.panels_with_errors.contains(&InputPanel::Include) {
1251                    theme.search.invalid_include_exclude_editor
1252                } else {
1253                    theme.search.include_exclude_editor.input.container
1254                };
1255
1256            let exclude_container_style =
1257                if search.panels_with_errors.contains(&InputPanel::Exclude) {
1258                    theme.search.invalid_include_exclude_editor
1259                } else {
1260                    theme.search.include_exclude_editor.input.container
1261                };
1262
1263            let included_files_view = ChildView::new(&search.included_files_editor, cx)
1264                .contained()
1265                .flex(1., true);
1266            let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
1267                .contained()
1268                .flex(1., true);
1269            let filters = search.filters_enabled.then(|| {
1270                Flex::row()
1271                    .with_child(
1272                        included_files_view
1273                            .contained()
1274                            .with_style(include_container_style)
1275                            .constrained()
1276                            .with_height(theme.search.search_bar_row_height)
1277                            .with_min_width(theme.search.include_exclude_editor.min_width)
1278                            .with_max_width(theme.search.include_exclude_editor.max_width),
1279                    )
1280                    .with_child(
1281                        excluded_files_view
1282                            .contained()
1283                            .with_style(exclude_container_style)
1284                            .constrained()
1285                            .with_height(theme.search.search_bar_row_height)
1286                            .with_min_width(theme.search.include_exclude_editor.min_width)
1287                            .with_max_width(theme.search.include_exclude_editor.max_width),
1288                    )
1289                    .contained()
1290                    .with_padding_top(theme.workspace.toolbar.container.padding.bottom)
1291            });
1292
1293            let editor_column = Flex::column()
1294                .with_child(
1295                    query
1296                        .contained()
1297                        .with_style(query_container_style)
1298                        .constrained()
1299                        .with_min_width(theme.search.editor.min_width)
1300                        .with_max_width(theme.search.editor.max_width)
1301                        .with_height(theme.search.search_bar_row_height)
1302                        .flex(1., false),
1303                )
1304                .with_children(filters)
1305                .flex(1., false);
1306
1307            let matches = search.active_match_index.map(|match_ix| {
1308                Label::new(
1309                    format!(
1310                        "{}/{}",
1311                        match_ix + 1,
1312                        search.model.read(cx).match_ranges.len()
1313                    ),
1314                    theme.search.match_index.text.clone(),
1315                )
1316                .contained()
1317                .with_style(theme.search.match_index.container)
1318                .aligned()
1319            });
1320
1321            let search_button_for_mode = |mode, cx: &mut ViewContext<ProjectSearchBar>| {
1322                let is_active = if let Some(search) = self.active_project_search.as_ref() {
1323                    let search = search.read(cx);
1324                    search.current_mode == mode
1325                } else {
1326                    false
1327                };
1328                render_search_mode_button(
1329                    mode,
1330                    is_active,
1331                    move |_, this, cx| {
1332                        this.activate_search_mode(mode, cx);
1333                    },
1334                    cx,
1335                )
1336            };
1337            let is_active = search.active_match_index.is_some();
1338
1339            let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
1340                render_nav_button(
1341                    label,
1342                    direction,
1343                    is_active,
1344                    move |_, this, cx| {
1345                        if let Some(search) = this.active_project_search.as_ref() {
1346                            search.update(cx, |search, cx| search.select_match(direction, cx));
1347                        }
1348                    },
1349                    cx,
1350                )
1351            };
1352
1353            let nav_column = Flex::row()
1354                .with_child(nav_button_for_direction("<", Direction::Prev, cx))
1355                .with_child(nav_button_for_direction(">", Direction::Next, cx))
1356                .with_child(Flex::row().with_children(matches))
1357                .constrained()
1358                .with_height(theme.search.search_bar_row_height);
1359
1360            let mode_column = Flex::row()
1361                .with_child(
1362                    Flex::row()
1363                        .with_child(search_button_for_mode(SearchMode::Text, cx))
1364                        .with_child(search_button_for_mode(SearchMode::Regex, cx))
1365                        .contained()
1366                        .with_style(theme.search.modes_container),
1367                )
1368                .with_child(super::search_bar::render_close_button(
1369                    "Dismiss Project Search",
1370                    &theme.search,
1371                    cx,
1372                    |_, this, cx| {
1373                        if let Some(search) = this.active_project_search.as_mut() {
1374                            search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss))
1375                        }
1376                    },
1377                    None,
1378                ))
1379                .constrained()
1380                .with_height(theme.search.search_bar_row_height)
1381                .aligned()
1382                .right()
1383                .top()
1384                .flex_float();
1385
1386            Flex::row()
1387                .with_child(editor_column)
1388                .with_child(nav_column)
1389                .with_child(mode_column)
1390                .contained()
1391                .with_style(theme.search.container)
1392                .into_any_named("project search")
1393        } else {
1394            Empty::new().into_any()
1395        }
1396    }
1397}
1398
1399impl ToolbarItemView for ProjectSearchBar {
1400    fn set_active_pane_item(
1401        &mut self,
1402        active_pane_item: Option<&dyn ItemHandle>,
1403        cx: &mut ViewContext<Self>,
1404    ) -> ToolbarItemLocation {
1405        cx.notify();
1406        self.subscription = None;
1407        self.active_project_search = None;
1408        if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
1409            self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
1410            self.active_project_search = Some(search);
1411            ToolbarItemLocation::PrimaryLeft {
1412                flex: Some((1., false)),
1413            }
1414        } else {
1415            ToolbarItemLocation::Hidden
1416        }
1417    }
1418
1419    fn row_count(&self, cx: &ViewContext<Self>) -> usize {
1420        self.active_project_search
1421            .as_ref()
1422            .map(|search| {
1423                let offset = search.read(cx).filters_enabled as usize;
1424                2 + offset
1425            })
1426            .unwrap_or_else(|| 2)
1427    }
1428}
1429
1430#[cfg(test)]
1431pub mod tests {
1432    use super::*;
1433    use editor::DisplayPoint;
1434    use gpui::{color::Color, executor::Deterministic, TestAppContext};
1435    use project::FakeFs;
1436    use semantic_index::semantic_index_settings::SemanticIndexSettings;
1437    use serde_json::json;
1438    use settings::SettingsStore;
1439    use std::sync::Arc;
1440    use theme::ThemeSettings;
1441
1442    #[gpui::test]
1443    async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1444        init_test(cx);
1445
1446        let fs = FakeFs::new(cx.background());
1447        fs.insert_tree(
1448            "/dir",
1449            json!({
1450                "one.rs": "const ONE: usize = 1;",
1451                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1452                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1453                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1454            }),
1455        )
1456        .await;
1457        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1458        let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
1459        let search_view = cx
1460            .add_window(|cx| ProjectSearchView::new(search.clone(), cx))
1461            .root(cx);
1462
1463        search_view.update(cx, |search_view, cx| {
1464            search_view
1465                .query_editor
1466                .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1467            search_view.search(cx);
1468        });
1469        deterministic.run_until_parked();
1470        search_view.update(cx, |search_view, cx| {
1471            assert_eq!(
1472                search_view
1473                    .results_editor
1474                    .update(cx, |editor, cx| editor.display_text(cx)),
1475                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
1476            );
1477            assert_eq!(
1478                search_view
1479                    .results_editor
1480                    .update(cx, |editor, cx| editor.all_background_highlights(cx)),
1481                &[
1482                    (
1483                        DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
1484                        Color::red()
1485                    ),
1486                    (
1487                        DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
1488                        Color::red()
1489                    ),
1490                    (
1491                        DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
1492                        Color::red()
1493                    )
1494                ]
1495            );
1496            assert_eq!(search_view.active_match_index, Some(0));
1497            assert_eq!(
1498                search_view
1499                    .results_editor
1500                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1501                [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1502            );
1503
1504            search_view.select_match(Direction::Next, cx);
1505        });
1506
1507        search_view.update(cx, |search_view, cx| {
1508            assert_eq!(search_view.active_match_index, Some(1));
1509            assert_eq!(
1510                search_view
1511                    .results_editor
1512                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1513                [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1514            );
1515            search_view.select_match(Direction::Next, cx);
1516        });
1517
1518        search_view.update(cx, |search_view, cx| {
1519            assert_eq!(search_view.active_match_index, Some(2));
1520            assert_eq!(
1521                search_view
1522                    .results_editor
1523                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1524                [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1525            );
1526            search_view.select_match(Direction::Next, cx);
1527        });
1528
1529        search_view.update(cx, |search_view, cx| {
1530            assert_eq!(search_view.active_match_index, Some(0));
1531            assert_eq!(
1532                search_view
1533                    .results_editor
1534                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1535                [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
1536            );
1537            search_view.select_match(Direction::Prev, cx);
1538        });
1539
1540        search_view.update(cx, |search_view, cx| {
1541            assert_eq!(search_view.active_match_index, Some(2));
1542            assert_eq!(
1543                search_view
1544                    .results_editor
1545                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1546                [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
1547            );
1548            search_view.select_match(Direction::Prev, cx);
1549        });
1550
1551        search_view.update(cx, |search_view, cx| {
1552            assert_eq!(search_view.active_match_index, Some(1));
1553            assert_eq!(
1554                search_view
1555                    .results_editor
1556                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
1557                [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
1558            );
1559        });
1560    }
1561
1562    #[gpui::test]
1563    async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1564        init_test(cx);
1565
1566        let fs = FakeFs::new(cx.background());
1567        fs.insert_tree(
1568            "/dir",
1569            json!({
1570                "one.rs": "const ONE: usize = 1;",
1571                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1572                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1573                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1574            }),
1575        )
1576        .await;
1577        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1578        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1579        let workspace = window.root(cx);
1580
1581        let active_item = cx.read(|cx| {
1582            workspace
1583                .read(cx)
1584                .active_pane()
1585                .read(cx)
1586                .active_item()
1587                .and_then(|item| item.downcast::<ProjectSearchView>())
1588        });
1589        assert!(
1590            active_item.is_none(),
1591            "Expected no search panel to be active, but got: {active_item:?}"
1592        );
1593
1594        workspace.update(cx, |workspace, cx| {
1595            ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1596        });
1597
1598        let Some(search_view) = cx.read(|cx| {
1599            workspace
1600                .read(cx)
1601                .active_pane()
1602                .read(cx)
1603                .active_item()
1604                .and_then(|item| item.downcast::<ProjectSearchView>())
1605        }) else {
1606            panic!("Search view expected to appear after new search event trigger")
1607        };
1608        let search_view_id = search_view.id();
1609
1610        cx.spawn(|mut cx| async move {
1611            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
1612        })
1613        .detach();
1614        deterministic.run_until_parked();
1615        search_view.update(cx, |search_view, cx| {
1616            assert!(
1617                search_view.query_editor.is_focused(cx),
1618                "Empty search view should be focused after the toggle focus event: no results panel to focus on",
1619            );
1620        });
1621
1622        search_view.update(cx, |search_view, cx| {
1623            let query_editor = &search_view.query_editor;
1624            assert!(
1625                query_editor.is_focused(cx),
1626                "Search view should be focused after the new search view is activated",
1627            );
1628            let query_text = query_editor.read(cx).text(cx);
1629            assert!(
1630                query_text.is_empty(),
1631                "New search query should be empty but got '{query_text}'",
1632            );
1633            let results_text = search_view
1634                .results_editor
1635                .update(cx, |editor, cx| editor.display_text(cx));
1636            assert!(
1637                results_text.is_empty(),
1638                "Empty search view should have no results but got '{results_text}'"
1639            );
1640        });
1641
1642        search_view.update(cx, |search_view, cx| {
1643            search_view.query_editor.update(cx, |query_editor, cx| {
1644                query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
1645            });
1646            search_view.search(cx);
1647        });
1648        deterministic.run_until_parked();
1649        search_view.update(cx, |search_view, cx| {
1650            let results_text = search_view
1651                .results_editor
1652                .update(cx, |editor, cx| editor.display_text(cx));
1653            assert!(
1654                results_text.is_empty(),
1655                "Search view for mismatching query should have no results but got '{results_text}'"
1656            );
1657            assert!(
1658                search_view.query_editor.is_focused(cx),
1659                "Search view should be focused after mismatching query had been used in search",
1660            );
1661        });
1662        cx.spawn(
1663            |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
1664        )
1665        .detach();
1666        deterministic.run_until_parked();
1667        search_view.update(cx, |search_view, cx| {
1668            assert!(
1669                search_view.query_editor.is_focused(cx),
1670                "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
1671            );
1672        });
1673
1674        search_view.update(cx, |search_view, cx| {
1675            search_view
1676                .query_editor
1677                .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1678            search_view.search(cx);
1679        });
1680        deterministic.run_until_parked();
1681        search_view.update(cx, |search_view, cx| {
1682            assert_eq!(
1683                search_view
1684                    .results_editor
1685                    .update(cx, |editor, cx| editor.display_text(cx)),
1686                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1687                "Search view results should match the query"
1688            );
1689            assert!(
1690                search_view.results_editor.is_focused(cx),
1691                "Search view with mismatching query should be focused after search results are available",
1692            );
1693        });
1694        cx.spawn(|mut cx| async move {
1695            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
1696        })
1697        .detach();
1698        deterministic.run_until_parked();
1699        search_view.update(cx, |search_view, cx| {
1700            assert!(
1701                search_view.results_editor.is_focused(cx),
1702                "Search view with matching query should still have its results editor focused after the toggle focus event",
1703            );
1704        });
1705
1706        workspace.update(cx, |workspace, cx| {
1707            ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1708        });
1709        search_view.update(cx, |search_view, cx| {
1710            assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
1711            assert_eq!(
1712                search_view
1713                    .results_editor
1714                    .update(cx, |editor, cx| editor.display_text(cx)),
1715                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1716                "Results should be unchanged after search view 2nd open in a row"
1717            );
1718            assert!(
1719                search_view.query_editor.is_focused(cx),
1720                "Focus should be moved into query editor again after search view 2nd open in a row"
1721            );
1722        });
1723
1724        cx.spawn(|mut cx| async move {
1725            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
1726        })
1727        .detach();
1728        deterministic.run_until_parked();
1729        search_view.update(cx, |search_view, cx| {
1730            assert!(
1731                search_view.results_editor.is_focused(cx),
1732                "Search view with matching query should switch focus to the results editor after the toggle focus event",
1733            );
1734        });
1735    }
1736
1737    #[gpui::test]
1738    async fn test_new_project_search_in_directory(
1739        deterministic: Arc<Deterministic>,
1740        cx: &mut TestAppContext,
1741    ) {
1742        init_test(cx);
1743
1744        let fs = FakeFs::new(cx.background());
1745        fs.insert_tree(
1746            "/dir",
1747            json!({
1748                "a": {
1749                    "one.rs": "const ONE: usize = 1;",
1750                    "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1751                },
1752                "b": {
1753                    "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1754                    "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1755                },
1756            }),
1757        )
1758        .await;
1759        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1760        let worktree_id = project.read_with(cx, |project, cx| {
1761            project.worktrees(cx).next().unwrap().read(cx).id()
1762        });
1763        let workspace = cx
1764            .add_window(|cx| Workspace::test_new(project, cx))
1765            .root(cx);
1766
1767        let active_item = cx.read(|cx| {
1768            workspace
1769                .read(cx)
1770                .active_pane()
1771                .read(cx)
1772                .active_item()
1773                .and_then(|item| item.downcast::<ProjectSearchView>())
1774        });
1775        assert!(
1776            active_item.is_none(),
1777            "Expected no search panel to be active, but got: {active_item:?}"
1778        );
1779
1780        let one_file_entry = cx.update(|cx| {
1781            workspace
1782                .read(cx)
1783                .project()
1784                .read(cx)
1785                .entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
1786                .expect("no entry for /a/one.rs file")
1787        });
1788        assert!(one_file_entry.is_file());
1789        workspace.update(cx, |workspace, cx| {
1790            ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
1791        });
1792        let active_search_entry = cx.read(|cx| {
1793            workspace
1794                .read(cx)
1795                .active_pane()
1796                .read(cx)
1797                .active_item()
1798                .and_then(|item| item.downcast::<ProjectSearchView>())
1799        });
1800        assert!(
1801            active_search_entry.is_none(),
1802            "Expected no search panel to be active for file entry"
1803        );
1804
1805        let a_dir_entry = cx.update(|cx| {
1806            workspace
1807                .read(cx)
1808                .project()
1809                .read(cx)
1810                .entry_for_path(&(worktree_id, "a").into(), cx)
1811                .expect("no entry for /a/ directory")
1812        });
1813        assert!(a_dir_entry.is_dir());
1814        workspace.update(cx, |workspace, cx| {
1815            ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
1816        });
1817
1818        let Some(search_view) = cx.read(|cx| {
1819            workspace
1820                .read(cx)
1821                .active_pane()
1822                .read(cx)
1823                .active_item()
1824                .and_then(|item| item.downcast::<ProjectSearchView>())
1825        }) else {
1826            panic!("Search view expected to appear after new search in directory event trigger")
1827        };
1828        deterministic.run_until_parked();
1829        search_view.update(cx, |search_view, cx| {
1830            assert!(
1831                search_view.query_editor.is_focused(cx),
1832                "On new search in directory, focus should be moved into query editor"
1833            );
1834            search_view.excluded_files_editor.update(cx, |editor, cx| {
1835                assert!(
1836                    editor.display_text(cx).is_empty(),
1837                    "New search in directory should not have any excluded files"
1838                );
1839            });
1840            search_view.included_files_editor.update(cx, |editor, cx| {
1841                assert_eq!(
1842                    editor.display_text(cx),
1843                    a_dir_entry.path.to_str().unwrap(),
1844                    "New search in directory should have included dir entry path"
1845                );
1846            });
1847        });
1848
1849        search_view.update(cx, |search_view, cx| {
1850            search_view
1851                .query_editor
1852                .update(cx, |query_editor, cx| query_editor.set_text("const", cx));
1853            search_view.search(cx);
1854        });
1855        deterministic.run_until_parked();
1856        search_view.update(cx, |search_view, cx| {
1857            assert_eq!(
1858                search_view
1859                    .results_editor
1860                    .update(cx, |editor, cx| editor.display_text(cx)),
1861                "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
1862                "New search in directory should have a filter that matches a certain directory"
1863            );
1864        });
1865    }
1866
1867    #[gpui::test]
1868    async fn test_search_query_history(cx: &mut TestAppContext) {
1869        init_test(cx);
1870
1871        let fs = FakeFs::new(cx.background());
1872        fs.insert_tree(
1873            "/dir",
1874            json!({
1875                "one.rs": "const ONE: usize = 1;",
1876                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1877                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
1878                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
1879            }),
1880        )
1881        .await;
1882        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
1883        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1884        let workspace = window.root(cx);
1885        workspace.update(cx, |workspace, cx| {
1886            ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
1887        });
1888
1889        let search_view = cx.read(|cx| {
1890            workspace
1891                .read(cx)
1892                .active_pane()
1893                .read(cx)
1894                .active_item()
1895                .and_then(|item| item.downcast::<ProjectSearchView>())
1896                .expect("Search view expected to appear after new search event trigger")
1897        });
1898
1899        let search_bar = window.add_view(cx, |cx| {
1900            let mut search_bar = ProjectSearchBar::new();
1901            search_bar.set_active_pane_item(Some(&search_view), cx);
1902            // search_bar.show(cx);
1903            search_bar
1904        });
1905
1906        // Add 3 search items into the history + another unsubmitted one.
1907        search_view.update(cx, |search_view, cx| {
1908            search_view.search_options = SearchOptions::CASE_SENSITIVE;
1909            search_view
1910                .query_editor
1911                .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
1912            search_view.search(cx);
1913        });
1914        cx.foreground().run_until_parked();
1915        search_view.update(cx, |search_view, cx| {
1916            search_view
1917                .query_editor
1918                .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
1919            search_view.search(cx);
1920        });
1921        cx.foreground().run_until_parked();
1922        search_view.update(cx, |search_view, cx| {
1923            search_view
1924                .query_editor
1925                .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
1926            search_view.search(cx);
1927        });
1928        cx.foreground().run_until_parked();
1929        search_view.update(cx, |search_view, cx| {
1930            search_view.query_editor.update(cx, |query_editor, cx| {
1931                query_editor.set_text("JUST_TEXT_INPUT", cx)
1932            });
1933        });
1934        cx.foreground().run_until_parked();
1935
1936        // Ensure that the latest input with search settings is active.
1937        search_view.update(cx, |search_view, cx| {
1938            assert_eq!(
1939                search_view.query_editor.read(cx).text(cx),
1940                "JUST_TEXT_INPUT"
1941            );
1942            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1943        });
1944
1945        // Next history query after the latest should set the query to the empty string.
1946        search_bar.update(cx, |search_bar, cx| {
1947            search_bar.next_history_query(&NextHistoryQuery, cx);
1948        });
1949        search_view.update(cx, |search_view, cx| {
1950            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
1951            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1952        });
1953        search_bar.update(cx, |search_bar, cx| {
1954            search_bar.next_history_query(&NextHistoryQuery, cx);
1955        });
1956        search_view.update(cx, |search_view, cx| {
1957            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
1958            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1959        });
1960
1961        // First previous query for empty current query should set the query to the latest submitted one.
1962        search_bar.update(cx, |search_bar, cx| {
1963            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1964        });
1965        search_view.update(cx, |search_view, cx| {
1966            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
1967            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1968        });
1969
1970        // Further previous items should go over the history in reverse order.
1971        search_bar.update(cx, |search_bar, cx| {
1972            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1973        });
1974        search_view.update(cx, |search_view, cx| {
1975            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
1976            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1977        });
1978
1979        // Previous items should never go behind the first history item.
1980        search_bar.update(cx, |search_bar, cx| {
1981            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1982        });
1983        search_view.update(cx, |search_view, cx| {
1984            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
1985            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1986        });
1987        search_bar.update(cx, |search_bar, cx| {
1988            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
1989        });
1990        search_view.update(cx, |search_view, cx| {
1991            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
1992            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
1993        });
1994
1995        // Next items should go over the history in the original order.
1996        search_bar.update(cx, |search_bar, cx| {
1997            search_bar.next_history_query(&NextHistoryQuery, cx);
1998        });
1999        search_view.update(cx, |search_view, cx| {
2000            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
2001            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2002        });
2003
2004        search_view.update(cx, |search_view, cx| {
2005            search_view
2006                .query_editor
2007                .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
2008            search_view.search(cx);
2009        });
2010        cx.foreground().run_until_parked();
2011        search_view.update(cx, |search_view, cx| {
2012            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
2013            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2014        });
2015
2016        // New search input should add another entry to history and move the selection to the end of the history.
2017        search_bar.update(cx, |search_bar, cx| {
2018            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2019        });
2020        search_view.update(cx, |search_view, cx| {
2021            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
2022            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2023        });
2024        search_bar.update(cx, |search_bar, cx| {
2025            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
2026        });
2027        search_view.update(cx, |search_view, cx| {
2028            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
2029            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2030        });
2031        search_bar.update(cx, |search_bar, cx| {
2032            search_bar.next_history_query(&NextHistoryQuery, cx);
2033        });
2034        search_view.update(cx, |search_view, cx| {
2035            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
2036            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2037        });
2038        search_bar.update(cx, |search_bar, cx| {
2039            search_bar.next_history_query(&NextHistoryQuery, cx);
2040        });
2041        search_view.update(cx, |search_view, cx| {
2042            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
2043            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2044        });
2045        search_bar.update(cx, |search_bar, cx| {
2046            search_bar.next_history_query(&NextHistoryQuery, cx);
2047        });
2048        search_view.update(cx, |search_view, cx| {
2049            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
2050            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
2051        });
2052    }
2053
2054    pub fn init_test(cx: &mut TestAppContext) {
2055        cx.foreground().forbid_parking();
2056        let fonts = cx.font_cache();
2057        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
2058        theme.search.match_background = Color::red();
2059
2060        cx.update(|cx| {
2061            cx.set_global(SettingsStore::test(cx));
2062            cx.set_global(ActiveSearches::default());
2063            settings::register::<SemanticIndexSettings>(cx);
2064
2065            theme::init((), cx);
2066            cx.update_global::<SettingsStore, _, _>(|store, _| {
2067                let mut settings = store.get::<ThemeSettings>(None).clone();
2068                settings.theme = Arc::new(theme);
2069                store.override_global(settings)
2070            });
2071
2072            language::init(cx);
2073            client::init_settings(cx);
2074            editor::init(cx);
2075            workspace::init_settings(cx);
2076            Project::init_settings(cx);
2077            super::init(cx);
2078        });
2079    }
2080}