project_search.rs

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