project_search.rs

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