file_finder.rs

   1use collections::HashMap;
   2use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
   3use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
   4use gpui::{
   5    actions, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
   6    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
   7};
   8use picker::{Picker, PickerDelegate};
   9use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
  10use std::{
  11    path::{Path, PathBuf},
  12    sync::{
  13        atomic::{self, AtomicBool},
  14        Arc,
  15    },
  16};
  17use text::Point;
  18use ui::{prelude::*, HighlightedLabel, ListItem};
  19use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
  20use workspace::{ModalView, Workspace};
  21
  22actions!(file_finder, [Toggle]);
  23
  24impl ModalView for FileFinder {}
  25
  26pub struct FileFinder {
  27    picker: View<Picker<FileFinderDelegate>>,
  28}
  29
  30pub fn init(cx: &mut AppContext) {
  31    cx.observe_new_views(FileFinder::register).detach();
  32}
  33
  34impl FileFinder {
  35    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  36        workspace.register_action(|workspace, _: &Toggle, cx| {
  37            let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
  38                Self::open(workspace, cx);
  39                return;
  40            };
  41
  42            file_finder.update(cx, |file_finder, cx| {
  43                file_finder
  44                    .picker
  45                    .update(cx, |picker, cx| picker.cycle_selection(cx))
  46            });
  47        });
  48    }
  49
  50    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
  51        let project = workspace.project().read(cx);
  52
  53        let currently_opened_path = workspace
  54            .active_item(cx)
  55            .and_then(|item| item.project_path(cx))
  56            .map(|project_path| {
  57                let abs_path = project
  58                    .worktree_for_id(project_path.worktree_id, cx)
  59                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
  60                FoundPath::new(project_path, abs_path)
  61            });
  62
  63        // if exists, bubble the currently opened path to the top
  64        let history_items = currently_opened_path
  65            .clone()
  66            .into_iter()
  67            .chain(
  68                workspace
  69                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
  70                    .into_iter()
  71                    .filter(|(history_path, _)| {
  72                        Some(history_path)
  73                            != currently_opened_path
  74                                .as_ref()
  75                                .map(|found_path| &found_path.project)
  76                    })
  77                    .filter(|(_, history_abs_path)| {
  78                        history_abs_path.as_ref()
  79                            != currently_opened_path
  80                                .as_ref()
  81                                .and_then(|found_path| found_path.absolute.as_ref())
  82                    })
  83                    .filter(|(_, history_abs_path)| match history_abs_path {
  84                        Some(abs_path) => history_file_exists(abs_path),
  85                        None => true,
  86                    })
  87                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
  88            )
  89            .collect();
  90
  91        let project = workspace.project().clone();
  92        let weak_workspace = cx.view().downgrade();
  93        workspace.toggle_modal(cx, |cx| {
  94            let delegate = FileFinderDelegate::new(
  95                cx.view().downgrade(),
  96                weak_workspace,
  97                project,
  98                currently_opened_path,
  99                history_items,
 100                cx,
 101            );
 102
 103            FileFinder::new(delegate, cx)
 104        });
 105    }
 106
 107    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
 108        Self {
 109            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
 110        }
 111    }
 112}
 113
 114impl EventEmitter<DismissEvent> for FileFinder {}
 115impl FocusableView for FileFinder {
 116    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 117        self.picker.focus_handle(cx)
 118    }
 119}
 120impl Render for FileFinder {
 121    type Element = Div;
 122
 123    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
 124        v_stack().w(rems(34.)).child(self.picker.clone())
 125    }
 126}
 127
 128pub struct FileFinderDelegate {
 129    file_finder: WeakView<FileFinder>,
 130    workspace: WeakView<Workspace>,
 131    project: Model<Project>,
 132    search_count: usize,
 133    latest_search_id: usize,
 134    latest_search_did_cancel: bool,
 135    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
 136    currently_opened_path: Option<FoundPath>,
 137    matches: Matches,
 138    selected_index: Option<usize>,
 139    cancel_flag: Arc<AtomicBool>,
 140    history_items: Vec<FoundPath>,
 141}
 142
 143#[derive(Debug, Default)]
 144struct Matches {
 145    history: Vec<(FoundPath, Option<PathMatch>)>,
 146    search: Vec<PathMatch>,
 147}
 148
 149#[derive(Debug)]
 150enum Match<'a> {
 151    History(&'a FoundPath, Option<&'a PathMatch>),
 152    Search(&'a PathMatch),
 153}
 154
 155impl Matches {
 156    fn len(&self) -> usize {
 157        self.history.len() + self.search.len()
 158    }
 159
 160    fn get(&self, index: usize) -> Option<Match<'_>> {
 161        if index < self.history.len() {
 162            self.history
 163                .get(index)
 164                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
 165        } else {
 166            self.search
 167                .get(index - self.history.len())
 168                .map(Match::Search)
 169        }
 170    }
 171
 172    fn push_new_matches(
 173        &mut self,
 174        history_items: &Vec<FoundPath>,
 175        query: &PathLikeWithPosition<FileSearchQuery>,
 176        mut new_search_matches: Vec<PathMatch>,
 177        extend_old_matches: bool,
 178    ) {
 179        let matching_history_paths = matching_history_item_paths(history_items, query);
 180        new_search_matches
 181            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
 182        let history_items_to_show = history_items
 183            .iter()
 184            .filter_map(|history_item| {
 185                Some((
 186                    history_item.clone(),
 187                    Some(
 188                        matching_history_paths
 189                            .get(&history_item.project.path)?
 190                            .clone(),
 191                    ),
 192                ))
 193            })
 194            .collect::<Vec<_>>();
 195        self.history = history_items_to_show;
 196        if extend_old_matches {
 197            self.search
 198                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
 199            util::extend_sorted(
 200                &mut self.search,
 201                new_search_matches.into_iter(),
 202                100,
 203                |a, b| b.cmp(a),
 204            )
 205        } else {
 206            self.search = new_search_matches;
 207        }
 208    }
 209}
 210
 211fn matching_history_item_paths(
 212    history_items: &Vec<FoundPath>,
 213    query: &PathLikeWithPosition<FileSearchQuery>,
 214) -> HashMap<Arc<Path>, PathMatch> {
 215    let history_items_by_worktrees = history_items
 216        .iter()
 217        .filter_map(|found_path| {
 218            let candidate = PathMatchCandidate {
 219                path: &found_path.project.path,
 220                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
 221                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
 222                // it would be shown first always, despite the latter being a better match.
 223                char_bag: CharBag::from_iter(
 224                    found_path
 225                        .project
 226                        .path
 227                        .file_name()?
 228                        .to_string_lossy()
 229                        .to_lowercase()
 230                        .chars(),
 231                ),
 232            };
 233            Some((found_path.project.worktree_id, candidate))
 234        })
 235        .fold(
 236            HashMap::default(),
 237            |mut candidates, (worktree_id, new_candidate)| {
 238                candidates
 239                    .entry(worktree_id)
 240                    .or_insert_with(Vec::new)
 241                    .push(new_candidate);
 242                candidates
 243            },
 244        );
 245    let mut matching_history_paths = HashMap::default();
 246    for (worktree, candidates) in history_items_by_worktrees {
 247        let max_results = candidates.len() + 1;
 248        matching_history_paths.extend(
 249            fuzzy::match_fixed_path_set(
 250                candidates,
 251                worktree.to_usize(),
 252                query.path_like.path_query(),
 253                false,
 254                max_results,
 255            )
 256            .into_iter()
 257            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
 258        );
 259    }
 260    matching_history_paths
 261}
 262
 263#[derive(Debug, Clone, PartialEq, Eq)]
 264struct FoundPath {
 265    project: ProjectPath,
 266    absolute: Option<PathBuf>,
 267}
 268
 269impl FoundPath {
 270    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
 271        Self { project, absolute }
 272    }
 273}
 274
 275const MAX_RECENT_SELECTIONS: usize = 20;
 276
 277#[cfg(not(test))]
 278fn history_file_exists(abs_path: &PathBuf) -> bool {
 279    abs_path.exists()
 280}
 281
 282#[cfg(test)]
 283fn history_file_exists(abs_path: &PathBuf) -> bool {
 284    !abs_path.ends_with("nonexistent.rs")
 285}
 286
 287pub enum Event {
 288    Selected(ProjectPath),
 289    Dismissed,
 290}
 291
 292#[derive(Debug, Clone)]
 293struct FileSearchQuery {
 294    raw_query: String,
 295    file_query_end: Option<usize>,
 296}
 297
 298impl FileSearchQuery {
 299    fn path_query(&self) -> &str {
 300        match self.file_query_end {
 301            Some(file_path_end) => &self.raw_query[..file_path_end],
 302            None => &self.raw_query,
 303        }
 304    }
 305}
 306
 307impl FileFinderDelegate {
 308    fn new(
 309        file_finder: WeakView<FileFinder>,
 310        workspace: WeakView<Workspace>,
 311        project: Model<Project>,
 312        currently_opened_path: Option<FoundPath>,
 313        history_items: Vec<FoundPath>,
 314        cx: &mut ViewContext<FileFinder>,
 315    ) -> Self {
 316        cx.observe(&project, |file_finder, _, cx| {
 317            //todo!() We should probably not re-render on every project anything
 318            file_finder
 319                .picker
 320                .update(cx, |picker, cx| picker.refresh(cx))
 321        })
 322        .detach();
 323
 324        Self {
 325            file_finder,
 326            workspace,
 327            project,
 328            search_count: 0,
 329            latest_search_id: 0,
 330            latest_search_did_cancel: false,
 331            latest_search_query: None,
 332            currently_opened_path,
 333            matches: Matches::default(),
 334            selected_index: None,
 335            cancel_flag: Arc::new(AtomicBool::new(false)),
 336            history_items,
 337        }
 338    }
 339
 340    fn spawn_search(
 341        &mut self,
 342        query: PathLikeWithPosition<FileSearchQuery>,
 343        cx: &mut ViewContext<Picker<Self>>,
 344    ) -> Task<()> {
 345        let relative_to = self
 346            .currently_opened_path
 347            .as_ref()
 348            .map(|found_path| Arc::clone(&found_path.project.path));
 349        let worktrees = self
 350            .project
 351            .read(cx)
 352            .visible_worktrees(cx)
 353            .collect::<Vec<_>>();
 354        let include_root_name = worktrees.len() > 1;
 355        let candidate_sets = worktrees
 356            .into_iter()
 357            .map(|worktree| {
 358                let worktree = worktree.read(cx);
 359                PathMatchCandidateSet {
 360                    snapshot: worktree.snapshot(),
 361                    include_ignored: worktree
 362                        .root_entry()
 363                        .map_or(false, |entry| entry.is_ignored),
 364                    include_root_name,
 365                }
 366            })
 367            .collect::<Vec<_>>();
 368
 369        let search_id = util::post_inc(&mut self.search_count);
 370        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
 371        self.cancel_flag = Arc::new(AtomicBool::new(false));
 372        let cancel_flag = self.cancel_flag.clone();
 373        cx.spawn(|picker, mut cx| async move {
 374            let matches = fuzzy::match_path_sets(
 375                candidate_sets.as_slice(),
 376                query.path_like.path_query(),
 377                relative_to,
 378                false,
 379                100,
 380                &cancel_flag,
 381                cx.background_executor().clone(),
 382            )
 383            .await;
 384            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
 385            picker
 386                .update(&mut cx, |picker, cx| {
 387                    picker
 388                        .delegate
 389                        .set_search_matches(search_id, did_cancel, query, matches, cx)
 390                })
 391                .log_err();
 392        })
 393    }
 394
 395    fn set_search_matches(
 396        &mut self,
 397        search_id: usize,
 398        did_cancel: bool,
 399        query: PathLikeWithPosition<FileSearchQuery>,
 400        matches: Vec<PathMatch>,
 401        cx: &mut ViewContext<Picker<Self>>,
 402    ) {
 403        if search_id >= self.latest_search_id {
 404            self.latest_search_id = search_id;
 405            let extend_old_matches = self.latest_search_did_cancel
 406                && Some(query.path_like.path_query())
 407                    == self
 408                        .latest_search_query
 409                        .as_ref()
 410                        .map(|query| query.path_like.path_query());
 411            self.matches
 412                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
 413            self.latest_search_query = Some(query);
 414            self.latest_search_did_cancel = did_cancel;
 415            cx.notify();
 416        }
 417    }
 418
 419    fn labels_for_match(
 420        &self,
 421        path_match: Match,
 422        cx: &AppContext,
 423        ix: usize,
 424    ) -> (String, Vec<usize>, String, Vec<usize>) {
 425        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
 426            Match::History(found_path, found_path_match) => {
 427                let worktree_id = found_path.project.worktree_id;
 428                let project_relative_path = &found_path.project.path;
 429                let has_worktree = self
 430                    .project
 431                    .read(cx)
 432                    .worktree_for_id(worktree_id, cx)
 433                    .is_some();
 434
 435                if !has_worktree {
 436                    if let Some(absolute_path) = &found_path.absolute {
 437                        return (
 438                            absolute_path
 439                                .file_name()
 440                                .map_or_else(
 441                                    || project_relative_path.to_string_lossy(),
 442                                    |file_name| file_name.to_string_lossy(),
 443                                )
 444                                .to_string(),
 445                            Vec::new(),
 446                            absolute_path.to_string_lossy().to_string(),
 447                            Vec::new(),
 448                        );
 449                    }
 450                }
 451
 452                let mut path = Arc::clone(project_relative_path);
 453                if project_relative_path.as_ref() == Path::new("") {
 454                    if let Some(absolute_path) = &found_path.absolute {
 455                        path = Arc::from(absolute_path.as_path());
 456                    }
 457                }
 458
 459                let mut path_match = PathMatch {
 460                    score: ix as f64,
 461                    positions: Vec::new(),
 462                    worktree_id: worktree_id.to_usize(),
 463                    path,
 464                    path_prefix: "".into(),
 465                    distance_to_relative_ancestor: usize::MAX,
 466                };
 467                if let Some(found_path_match) = found_path_match {
 468                    path_match
 469                        .positions
 470                        .extend(found_path_match.positions.iter())
 471                }
 472
 473                self.labels_for_path_match(&path_match)
 474            }
 475            Match::Search(path_match) => self.labels_for_path_match(path_match),
 476        };
 477
 478        if file_name_positions.is_empty() {
 479            if let Some(user_home_path) = std::env::var("HOME").ok() {
 480                let user_home_path = user_home_path.trim();
 481                if !user_home_path.is_empty() {
 482                    if (&full_path).starts_with(user_home_path) {
 483                        return (
 484                            file_name,
 485                            file_name_positions,
 486                            full_path.replace(user_home_path, "~"),
 487                            full_path_positions,
 488                        );
 489                    }
 490                }
 491            }
 492        }
 493
 494        (
 495            file_name,
 496            file_name_positions,
 497            full_path,
 498            full_path_positions,
 499        )
 500    }
 501
 502    fn labels_for_path_match(
 503        &self,
 504        path_match: &PathMatch,
 505    ) -> (String, Vec<usize>, String, Vec<usize>) {
 506        let path = &path_match.path;
 507        let path_string = path.to_string_lossy();
 508        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
 509        let path_positions = path_match.positions.clone();
 510
 511        let file_name = path.file_name().map_or_else(
 512            || path_match.path_prefix.to_string(),
 513            |file_name| file_name.to_string_lossy().to_string(),
 514        );
 515        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
 516            - file_name.chars().count();
 517        let file_name_positions = path_positions
 518            .iter()
 519            .filter_map(|pos| {
 520                if pos >= &file_name_start {
 521                    Some(pos - file_name_start)
 522                } else {
 523                    None
 524                }
 525            })
 526            .collect();
 527
 528        (file_name, file_name_positions, full_path, path_positions)
 529    }
 530}
 531
 532impl PickerDelegate for FileFinderDelegate {
 533    type ListItem = ListItem;
 534
 535    fn placeholder_text(&self) -> Arc<str> {
 536        "Search project files...".into()
 537    }
 538
 539    fn match_count(&self) -> usize {
 540        self.matches.len()
 541    }
 542
 543    fn selected_index(&self) -> usize {
 544        self.selected_index.unwrap_or(0)
 545    }
 546
 547    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
 548        self.selected_index = Some(ix);
 549        cx.notify();
 550    }
 551
 552    fn update_matches(
 553        &mut self,
 554        raw_query: String,
 555        cx: &mut ViewContext<Picker<Self>>,
 556    ) -> Task<()> {
 557        let raw_query = raw_query.trim();
 558        if raw_query.is_empty() {
 559            let project = self.project.read(cx);
 560            self.latest_search_id = post_inc(&mut self.search_count);
 561            self.matches = Matches {
 562                history: self
 563                    .history_items
 564                    .iter()
 565                    .filter(|history_item| {
 566                        project
 567                            .worktree_for_id(history_item.project.worktree_id, cx)
 568                            .is_some()
 569                            || (project.is_local() && history_item.absolute.is_some())
 570                    })
 571                    .cloned()
 572                    .map(|p| (p, None))
 573                    .collect(),
 574                search: Vec::new(),
 575            };
 576            cx.notify();
 577            Task::ready(())
 578        } else {
 579            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
 580                Ok::<_, std::convert::Infallible>(FileSearchQuery {
 581                    raw_query: raw_query.to_owned(),
 582                    file_query_end: if path_like_str == raw_query {
 583                        None
 584                    } else {
 585                        Some(path_like_str.len())
 586                    },
 587                })
 588            })
 589            .expect("infallible");
 590            self.spawn_search(query, cx)
 591        }
 592    }
 593
 594    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
 595        if let Some(m) = self.matches.get(self.selected_index()) {
 596            if let Some(workspace) = self.workspace.upgrade() {
 597                let open_task = workspace.update(cx, move |workspace, cx| {
 598                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
 599                        if secondary {
 600                            workspace.split_path(project_path, cx)
 601                        } else {
 602                            workspace.open_path(project_path, None, true, cx)
 603                        }
 604                    };
 605                    match m {
 606                        Match::History(history_match, _) => {
 607                            let worktree_id = history_match.project.worktree_id;
 608                            if workspace
 609                                .project()
 610                                .read(cx)
 611                                .worktree_for_id(worktree_id, cx)
 612                                .is_some()
 613                            {
 614                                split_or_open(
 615                                    workspace,
 616                                    ProjectPath {
 617                                        worktree_id,
 618                                        path: Arc::clone(&history_match.project.path),
 619                                    },
 620                                    cx,
 621                                )
 622                            } else {
 623                                match history_match.absolute.as_ref() {
 624                                    Some(abs_path) => {
 625                                        if secondary {
 626                                            workspace.split_abs_path(
 627                                                abs_path.to_path_buf(),
 628                                                false,
 629                                                cx,
 630                                            )
 631                                        } else {
 632                                            workspace.open_abs_path(
 633                                                abs_path.to_path_buf(),
 634                                                false,
 635                                                cx,
 636                                            )
 637                                        }
 638                                    }
 639                                    None => split_or_open(
 640                                        workspace,
 641                                        ProjectPath {
 642                                            worktree_id,
 643                                            path: Arc::clone(&history_match.project.path),
 644                                        },
 645                                        cx,
 646                                    ),
 647                                }
 648                            }
 649                        }
 650                        Match::Search(m) => split_or_open(
 651                            workspace,
 652                            ProjectPath {
 653                                worktree_id: WorktreeId::from_usize(m.worktree_id),
 654                                path: m.path.clone(),
 655                            },
 656                            cx,
 657                        ),
 658                    }
 659                });
 660
 661                let row = self
 662                    .latest_search_query
 663                    .as_ref()
 664                    .and_then(|query| query.row)
 665                    .map(|row| row.saturating_sub(1));
 666                let col = self
 667                    .latest_search_query
 668                    .as_ref()
 669                    .and_then(|query| query.column)
 670                    .unwrap_or(0)
 671                    .saturating_sub(1);
 672                let finder = self.file_finder.clone();
 673
 674                cx.spawn(|_, mut cx| async move {
 675                    let item = open_task.await.log_err()?;
 676                    if let Some(row) = row {
 677                        if let Some(active_editor) = item.downcast::<Editor>() {
 678                            active_editor
 679                                .downgrade()
 680                                .update(&mut cx, |editor, cx| {
 681                                    let snapshot = editor.snapshot(cx).display_snapshot;
 682                                    let point = snapshot
 683                                        .buffer_snapshot
 684                                        .clip_point(Point::new(row, col), Bias::Left);
 685                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 686                                        s.select_ranges([point..point])
 687                                    });
 688                                })
 689                                .log_err();
 690                        }
 691                    }
 692                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
 693
 694                    Some(())
 695                })
 696                .detach();
 697            }
 698        }
 699    }
 700
 701    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
 702        self.file_finder
 703            .update(cx, |_, cx| cx.emit(DismissEvent))
 704            .log_err();
 705    }
 706
 707    fn render_match(
 708        &self,
 709        ix: usize,
 710        selected: bool,
 711        cx: &mut ViewContext<Picker<Self>>,
 712    ) -> Option<Self::ListItem> {
 713        let path_match = self
 714            .matches
 715            .get(ix)
 716            .expect("Invalid matches state: no element for index {ix}");
 717
 718        let (file_name, file_name_positions, full_path, full_path_positions) =
 719            self.labels_for_match(path_match, cx, ix);
 720
 721        Some(
 722            ListItem::new(ix).inset(true).selected(selected).child(
 723                v_stack()
 724                    .child(HighlightedLabel::new(file_name, file_name_positions))
 725                    .child(
 726                        HighlightedLabel::new(full_path, full_path_positions).color(Color::Muted),
 727                    ),
 728            ),
 729        )
 730    }
 731}
 732
 733#[cfg(test)]
 734mod tests {
 735    use std::{assert_eq, path::Path, time::Duration};
 736
 737    use super::*;
 738    use editor::Editor;
 739    use gpui::{Entity, TestAppContext, VisualTestContext};
 740    use menu::{Confirm, SelectNext};
 741    use serde_json::json;
 742    use workspace::{AppState, Workspace};
 743
 744    #[ctor::ctor]
 745    fn init_logger() {
 746        if std::env::var("RUST_LOG").is_ok() {
 747            env_logger::init();
 748        }
 749    }
 750
 751    #[gpui::test]
 752    async fn test_matching_paths(cx: &mut TestAppContext) {
 753        let app_state = init_test(cx);
 754        app_state
 755            .fs
 756            .as_fake()
 757            .insert_tree(
 758                "/root",
 759                json!({
 760                    "a": {
 761                        "banana": "",
 762                        "bandana": "",
 763                    }
 764                }),
 765            )
 766            .await;
 767
 768        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 769
 770        let (picker, workspace, cx) = build_find_picker(project, cx);
 771
 772        cx.simulate_input("bna");
 773        picker.update(cx, |picker, _| {
 774            assert_eq!(picker.delegate.matches.len(), 2);
 775        });
 776        cx.dispatch_action(SelectNext);
 777        cx.dispatch_action(Confirm);
 778        cx.read(|cx| {
 779            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 780            assert_eq!(active_editor.read(cx).title(cx), "bandana");
 781        });
 782
 783        for bandana_query in [
 784            "bandana",
 785            " bandana",
 786            "bandana ",
 787            " bandana ",
 788            " ndan ",
 789            " band ",
 790        ] {
 791            picker
 792                .update(cx, |picker, cx| {
 793                    picker
 794                        .delegate
 795                        .update_matches(bandana_query.to_string(), cx)
 796                })
 797                .await;
 798            picker.update(cx, |picker, _| {
 799                assert_eq!(
 800                    picker.delegate.matches.len(),
 801                    1,
 802                    "Wrong number of matches for bandana query '{bandana_query}'"
 803                );
 804            });
 805            cx.dispatch_action(SelectNext);
 806            cx.dispatch_action(Confirm);
 807            cx.read(|cx| {
 808                let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 809                assert_eq!(
 810                    active_editor.read(cx).title(cx),
 811                    "bandana",
 812                    "Wrong match for bandana query '{bandana_query}'"
 813                );
 814            });
 815        }
 816    }
 817
 818    #[gpui::test]
 819    async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 820        let app_state = init_test(cx);
 821
 822        let first_file_name = "first.rs";
 823        let first_file_contents = "// First Rust file";
 824        app_state
 825            .fs
 826            .as_fake()
 827            .insert_tree(
 828                "/src",
 829                json!({
 830                    "test": {
 831                        first_file_name: first_file_contents,
 832                        "second.rs": "// Second Rust file",
 833                    }
 834                }),
 835            )
 836            .await;
 837
 838        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 839
 840        let (picker, workspace, cx) = build_find_picker(project, cx);
 841
 842        let file_query = &first_file_name[..3];
 843        let file_row = 1;
 844        let file_column = 3;
 845        assert!(file_column <= first_file_contents.len());
 846        let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 847        picker
 848            .update(cx, |finder, cx| {
 849                finder
 850                    .delegate
 851                    .update_matches(query_inside_file.to_string(), cx)
 852            })
 853            .await;
 854        picker.update(cx, |finder, _| {
 855            let finder = &finder.delegate;
 856            assert_eq!(finder.matches.len(), 1);
 857            let latest_search_query = finder
 858                .latest_search_query
 859                .as_ref()
 860                .expect("Finder should have a query after the update_matches call");
 861            assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
 862            assert_eq!(
 863                latest_search_query.path_like.file_query_end,
 864                Some(file_query.len())
 865            );
 866            assert_eq!(latest_search_query.row, Some(file_row));
 867            assert_eq!(latest_search_query.column, Some(file_column as u32));
 868        });
 869
 870        cx.dispatch_action(SelectNext);
 871        cx.dispatch_action(Confirm);
 872
 873        let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 874        cx.executor().advance_clock(Duration::from_secs(2));
 875
 876        editor.update(cx, |editor, cx| {
 877                let all_selections = editor.selections.all_adjusted(cx);
 878                assert_eq!(
 879                    all_selections.len(),
 880                    1,
 881                    "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 882                );
 883                let caret_selection = all_selections.into_iter().next().unwrap();
 884                assert_eq!(caret_selection.start, caret_selection.end,
 885                    "Caret selection should have its start and end at the same position");
 886                assert_eq!(file_row, caret_selection.start.row + 1,
 887                    "Query inside file should get caret with the same focus row");
 888                assert_eq!(file_column, caret_selection.start.column as usize + 1,
 889                    "Query inside file should get caret with the same focus column");
 890            });
 891    }
 892
 893    #[gpui::test]
 894    async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 895        let app_state = init_test(cx);
 896
 897        let first_file_name = "first.rs";
 898        let first_file_contents = "// First Rust file";
 899        app_state
 900            .fs
 901            .as_fake()
 902            .insert_tree(
 903                "/src",
 904                json!({
 905                    "test": {
 906                        first_file_name: first_file_contents,
 907                        "second.rs": "// Second Rust file",
 908                    }
 909                }),
 910            )
 911            .await;
 912
 913        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 914
 915        let (picker, workspace, cx) = build_find_picker(project, cx);
 916
 917        let file_query = &first_file_name[..3];
 918        let file_row = 200;
 919        let file_column = 300;
 920        assert!(file_column > first_file_contents.len());
 921        let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 922        picker
 923            .update(cx, |picker, cx| {
 924                picker
 925                    .delegate
 926                    .update_matches(query_outside_file.to_string(), cx)
 927            })
 928            .await;
 929        picker.update(cx, |finder, _| {
 930            let delegate = &finder.delegate;
 931            assert_eq!(delegate.matches.len(), 1);
 932            let latest_search_query = delegate
 933                .latest_search_query
 934                .as_ref()
 935                .expect("Finder should have a query after the update_matches call");
 936            assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
 937            assert_eq!(
 938                latest_search_query.path_like.file_query_end,
 939                Some(file_query.len())
 940            );
 941            assert_eq!(latest_search_query.row, Some(file_row));
 942            assert_eq!(latest_search_query.column, Some(file_column as u32));
 943        });
 944
 945        cx.dispatch_action(SelectNext);
 946        cx.dispatch_action(Confirm);
 947
 948        let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 949        cx.executor().advance_clock(Duration::from_secs(2));
 950
 951        editor.update(cx, |editor, cx| {
 952                let all_selections = editor.selections.all_adjusted(cx);
 953                assert_eq!(
 954                    all_selections.len(),
 955                    1,
 956                    "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 957                );
 958                let caret_selection = all_selections.into_iter().next().unwrap();
 959                assert_eq!(caret_selection.start, caret_selection.end,
 960                    "Caret selection should have its start and end at the same position");
 961                assert_eq!(0, caret_selection.start.row,
 962                    "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 963                assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 964                    "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 965            });
 966    }
 967
 968    #[gpui::test]
 969    async fn test_matching_cancellation(cx: &mut TestAppContext) {
 970        let app_state = init_test(cx);
 971        app_state
 972            .fs
 973            .as_fake()
 974            .insert_tree(
 975                "/dir",
 976                json!({
 977                    "hello": "",
 978                    "goodbye": "",
 979                    "halogen-light": "",
 980                    "happiness": "",
 981                    "height": "",
 982                    "hi": "",
 983                    "hiccup": "",
 984                }),
 985            )
 986            .await;
 987
 988        let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 989
 990        let (picker, _, cx) = build_find_picker(project, cx);
 991
 992        let query = test_path_like("hi");
 993        picker
 994            .update(cx, |picker, cx| {
 995                picker.delegate.spawn_search(query.clone(), cx)
 996            })
 997            .await;
 998
 999        picker.update(cx, |picker, _cx| {
1000            assert_eq!(picker.delegate.matches.len(), 5)
1001        });
1002
1003        picker.update(cx, |picker, cx| {
1004            let delegate = &mut picker.delegate;
1005            assert!(
1006                delegate.matches.history.is_empty(),
1007                "Search matches expected"
1008            );
1009            let matches = delegate.matches.search.clone();
1010
1011            // Simulate a search being cancelled after the time limit,
1012            // returning only a subset of the matches that would have been found.
1013            drop(delegate.spawn_search(query.clone(), cx));
1014            delegate.set_search_matches(
1015                delegate.latest_search_id,
1016                true, // did-cancel
1017                query.clone(),
1018                vec![matches[1].clone(), matches[3].clone()],
1019                cx,
1020            );
1021
1022            // Simulate another cancellation.
1023            drop(delegate.spawn_search(query.clone(), cx));
1024            delegate.set_search_matches(
1025                delegate.latest_search_id,
1026                true, // did-cancel
1027                query.clone(),
1028                vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
1029                cx,
1030            );
1031
1032            assert!(
1033                delegate.matches.history.is_empty(),
1034                "Search matches expected"
1035            );
1036            assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
1037        });
1038    }
1039
1040    #[gpui::test]
1041    async fn test_ignored_files(cx: &mut TestAppContext) {
1042        let app_state = init_test(cx);
1043        app_state
1044            .fs
1045            .as_fake()
1046            .insert_tree(
1047                "/ancestor",
1048                json!({
1049                    ".gitignore": "ignored-root",
1050                    "ignored-root": {
1051                        "happiness": "",
1052                        "height": "",
1053                        "hi": "",
1054                        "hiccup": "",
1055                    },
1056                    "tracked-root": {
1057                        ".gitignore": "height",
1058                        "happiness": "",
1059                        "height": "",
1060                        "hi": "",
1061                        "hiccup": "",
1062                    },
1063                }),
1064            )
1065            .await;
1066
1067        let project = Project::test(
1068            app_state.fs.clone(),
1069            [
1070                "/ancestor/tracked-root".as_ref(),
1071                "/ancestor/ignored-root".as_ref(),
1072            ],
1073            cx,
1074        )
1075        .await;
1076
1077        let (picker, _, cx) = build_find_picker(project, cx);
1078
1079        picker
1080            .update(cx, |picker, cx| {
1081                picker.delegate.spawn_search(test_path_like("hi"), cx)
1082            })
1083            .await;
1084        picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
1085    }
1086
1087    #[gpui::test]
1088    async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1089        let app_state = init_test(cx);
1090        app_state
1091            .fs
1092            .as_fake()
1093            .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1094            .await;
1095
1096        let project = Project::test(
1097            app_state.fs.clone(),
1098            ["/root/the-parent-dir/the-file".as_ref()],
1099            cx,
1100        )
1101        .await;
1102
1103        let (picker, _, cx) = build_find_picker(project, cx);
1104
1105        // Even though there is only one worktree, that worktree's filename
1106        // is included in the matching, because the worktree is a single file.
1107        picker
1108            .update(cx, |picker, cx| {
1109                picker.delegate.spawn_search(test_path_like("thf"), cx)
1110            })
1111            .await;
1112        cx.read(|cx| {
1113            let picker = picker.read(cx);
1114            let delegate = &picker.delegate;
1115            assert!(
1116                delegate.matches.history.is_empty(),
1117                "Search matches expected"
1118            );
1119            let matches = delegate.matches.search.clone();
1120            assert_eq!(matches.len(), 1);
1121
1122            let (file_name, file_name_positions, full_path, full_path_positions) =
1123                delegate.labels_for_path_match(&matches[0]);
1124            assert_eq!(file_name, "the-file");
1125            assert_eq!(file_name_positions, &[0, 1, 4]);
1126            assert_eq!(full_path, "the-file");
1127            assert_eq!(full_path_positions, &[0, 1, 4]);
1128        });
1129
1130        // Since the worktree root is a file, searching for its name followed by a slash does
1131        // not match anything.
1132        picker
1133            .update(cx, |f, cx| {
1134                f.delegate.spawn_search(test_path_like("thf/"), cx)
1135            })
1136            .await;
1137        picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1138    }
1139
1140    #[gpui::test]
1141    async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1142        let app_state = init_test(cx);
1143        app_state
1144            .fs
1145            .as_fake()
1146            .insert_tree(
1147                "/root",
1148                json!({
1149                    "dir1": { "a.txt": "" },
1150                    "dir2": {
1151                        "a.txt": "",
1152                        "b.txt": ""
1153                    }
1154                }),
1155            )
1156            .await;
1157
1158        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1159        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1160
1161        let worktree_id = cx.read(|cx| {
1162            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1163            assert_eq!(worktrees.len(), 1);
1164            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1165        });
1166
1167        // When workspace has an active item, sort items which are closer to that item
1168        // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1169        // so that one should be sorted earlier
1170        let b_path = ProjectPath {
1171            worktree_id,
1172            path: Arc::from(Path::new("/root/dir2/b.txt")),
1173        };
1174        workspace
1175            .update(cx, |workspace, cx| {
1176                workspace.open_path(b_path, None, true, cx)
1177            })
1178            .await
1179            .unwrap();
1180        let finder = open_file_picker(&workspace, cx);
1181        finder
1182            .update(cx, |f, cx| {
1183                f.delegate.spawn_search(test_path_like("a.txt"), cx)
1184            })
1185            .await;
1186
1187        finder.update(cx, |f, _| {
1188            let delegate = &f.delegate;
1189            assert!(
1190                delegate.matches.history.is_empty(),
1191                "Search matches expected"
1192            );
1193            let matches = delegate.matches.search.clone();
1194            assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1195            assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1196        });
1197    }
1198
1199    #[gpui::test]
1200    async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1201        let app_state = init_test(cx);
1202        app_state
1203            .fs
1204            .as_fake()
1205            .insert_tree(
1206                "/root",
1207                json!({
1208                    "dir1": {},
1209                    "dir2": {
1210                        "dir3": {}
1211                    }
1212                }),
1213            )
1214            .await;
1215
1216        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1217        let (picker, _workspace, cx) = build_find_picker(project, cx);
1218
1219        picker
1220            .update(cx, |f, cx| {
1221                f.delegate.spawn_search(test_path_like("dir"), cx)
1222            })
1223            .await;
1224        cx.read(|cx| {
1225            let finder = picker.read(cx);
1226            assert_eq!(finder.delegate.matches.len(), 0);
1227        });
1228    }
1229
1230    #[gpui::test]
1231    async fn test_query_history(cx: &mut gpui::TestAppContext) {
1232        let app_state = init_test(cx);
1233
1234        app_state
1235            .fs
1236            .as_fake()
1237            .insert_tree(
1238                "/src",
1239                json!({
1240                    "test": {
1241                        "first.rs": "// First Rust file",
1242                        "second.rs": "// Second Rust file",
1243                        "third.rs": "// Third Rust file",
1244                    }
1245                }),
1246            )
1247            .await;
1248
1249        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1250        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1251        let worktree_id = cx.read(|cx| {
1252            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1253            assert_eq!(worktrees.len(), 1);
1254            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1255        });
1256
1257        // Open and close panels, getting their history items afterwards.
1258        // Ensure history items get populated with opened items, and items are kept in a certain order.
1259        // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1260        //
1261        // TODO: without closing, the opened items do not propagate their history changes for some reason
1262        // it does work in real app though, only tests do not propagate.
1263        workspace.update(cx, |_, cx| cx.focused());
1264
1265        let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1266        assert!(
1267            initial_history.is_empty(),
1268            "Should have no history before opening any files"
1269        );
1270
1271        let history_after_first =
1272            open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1273        assert_eq!(
1274            history_after_first,
1275            vec![FoundPath::new(
1276                ProjectPath {
1277                    worktree_id,
1278                    path: Arc::from(Path::new("test/first.rs")),
1279                },
1280                Some(PathBuf::from("/src/test/first.rs"))
1281            )],
1282            "Should show 1st opened item in the history when opening the 2nd item"
1283        );
1284
1285        let history_after_second =
1286            open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1287        assert_eq!(
1288            history_after_second,
1289            vec![
1290                FoundPath::new(
1291                    ProjectPath {
1292                        worktree_id,
1293                        path: Arc::from(Path::new("test/second.rs")),
1294                    },
1295                    Some(PathBuf::from("/src/test/second.rs"))
1296                ),
1297                FoundPath::new(
1298                    ProjectPath {
1299                        worktree_id,
1300                        path: Arc::from(Path::new("test/first.rs")),
1301                    },
1302                    Some(PathBuf::from("/src/test/first.rs"))
1303                ),
1304            ],
1305            "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1306    2nd item should be the first in the history, as the last opened."
1307        );
1308
1309        let history_after_third =
1310            open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1311        assert_eq!(
1312                history_after_third,
1313                vec![
1314                    FoundPath::new(
1315                        ProjectPath {
1316                            worktree_id,
1317                            path: Arc::from(Path::new("test/third.rs")),
1318                        },
1319                        Some(PathBuf::from("/src/test/third.rs"))
1320                    ),
1321                    FoundPath::new(
1322                        ProjectPath {
1323                            worktree_id,
1324                            path: Arc::from(Path::new("test/second.rs")),
1325                        },
1326                        Some(PathBuf::from("/src/test/second.rs"))
1327                    ),
1328                    FoundPath::new(
1329                        ProjectPath {
1330                            worktree_id,
1331                            path: Arc::from(Path::new("test/first.rs")),
1332                        },
1333                        Some(PathBuf::from("/src/test/first.rs"))
1334                    ),
1335                ],
1336                "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1337    3rd item should be the first in the history, as the last opened."
1338            );
1339
1340        let history_after_second_again =
1341            open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1342        assert_eq!(
1343                history_after_second_again,
1344                vec![
1345                    FoundPath::new(
1346                        ProjectPath {
1347                            worktree_id,
1348                            path: Arc::from(Path::new("test/second.rs")),
1349                        },
1350                        Some(PathBuf::from("/src/test/second.rs"))
1351                    ),
1352                    FoundPath::new(
1353                        ProjectPath {
1354                            worktree_id,
1355                            path: Arc::from(Path::new("test/third.rs")),
1356                        },
1357                        Some(PathBuf::from("/src/test/third.rs"))
1358                    ),
1359                    FoundPath::new(
1360                        ProjectPath {
1361                            worktree_id,
1362                            path: Arc::from(Path::new("test/first.rs")),
1363                        },
1364                        Some(PathBuf::from("/src/test/first.rs"))
1365                    ),
1366                ],
1367                "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1368    2nd item, as the last opened, 3rd item should go next as it was opened right before."
1369            );
1370    }
1371
1372    #[gpui::test]
1373    async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1374        let app_state = init_test(cx);
1375
1376        app_state
1377            .fs
1378            .as_fake()
1379            .insert_tree(
1380                "/src",
1381                json!({
1382                    "test": {
1383                        "first.rs": "// First Rust file",
1384                        "second.rs": "// Second Rust file",
1385                    }
1386                }),
1387            )
1388            .await;
1389
1390        app_state
1391            .fs
1392            .as_fake()
1393            .insert_tree(
1394                "/external-src",
1395                json!({
1396                    "test": {
1397                        "third.rs": "// Third Rust file",
1398                        "fourth.rs": "// Fourth Rust file",
1399                    }
1400                }),
1401            )
1402            .await;
1403
1404        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1405        cx.update(|cx| {
1406            project.update(cx, |project, cx| {
1407                project.find_or_create_local_worktree("/external-src", false, cx)
1408            })
1409        })
1410        .detach();
1411        cx.background_executor.run_until_parked();
1412
1413        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1414        let worktree_id = cx.read(|cx| {
1415            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1416            assert_eq!(worktrees.len(), 1,);
1417
1418            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1419        });
1420        workspace
1421            .update(cx, |workspace, cx| {
1422                workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1423            })
1424            .detach();
1425        cx.background_executor.run_until_parked();
1426        let external_worktree_id = cx.read(|cx| {
1427            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1428            assert_eq!(
1429                worktrees.len(),
1430                2,
1431                "External file should get opened in a new worktree"
1432            );
1433
1434            WorktreeId::from_usize(
1435                worktrees
1436                    .into_iter()
1437                    .find(|worktree| {
1438                        worktree.entity_id().as_u64() as usize != worktree_id.to_usize()
1439                    })
1440                    .expect("New worktree should have a different id")
1441                    .entity_id()
1442                    .as_u64() as usize,
1443            )
1444        });
1445        cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1446
1447        let initial_history_items =
1448            open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1449        assert_eq!(
1450            initial_history_items,
1451            vec![FoundPath::new(
1452                ProjectPath {
1453                    worktree_id: external_worktree_id,
1454                    path: Arc::from(Path::new("")),
1455                },
1456                Some(PathBuf::from("/external-src/test/third.rs"))
1457            )],
1458            "Should show external file with its full path in the history after it was open"
1459        );
1460
1461        let updated_history_items =
1462            open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1463        assert_eq!(
1464            updated_history_items,
1465            vec![
1466                FoundPath::new(
1467                    ProjectPath {
1468                        worktree_id,
1469                        path: Arc::from(Path::new("test/second.rs")),
1470                    },
1471                    Some(PathBuf::from("/src/test/second.rs"))
1472                ),
1473                FoundPath::new(
1474                    ProjectPath {
1475                        worktree_id: external_worktree_id,
1476                        path: Arc::from(Path::new("")),
1477                    },
1478                    Some(PathBuf::from("/external-src/test/third.rs"))
1479                ),
1480            ],
1481            "Should keep external file with history updates",
1482        );
1483    }
1484
1485    #[gpui::test]
1486    async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1487        let app_state = init_test(cx);
1488
1489        app_state
1490            .fs
1491            .as_fake()
1492            .insert_tree(
1493                "/src",
1494                json!({
1495                    "test": {
1496                        "first.rs": "// First Rust file",
1497                        "second.rs": "// Second Rust file",
1498                        "third.rs": "// Third Rust file",
1499                    }
1500                }),
1501            )
1502            .await;
1503
1504        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1505        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1506
1507        // generate some history to select from
1508        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1509        cx.executor().run_until_parked();
1510        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1511        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1512        let current_history =
1513            open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1514
1515        for expected_selected_index in 0..current_history.len() {
1516            cx.dispatch_action(Toggle);
1517            let picker = active_file_picker(&workspace, cx);
1518            let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1519            assert_eq!(
1520                selected_index, expected_selected_index,
1521                "Should select the next item in the history"
1522            );
1523        }
1524
1525        cx.dispatch_action(Toggle);
1526        let selected_index = workspace.update(cx, |workspace, cx| {
1527            workspace
1528                .active_modal::<FileFinder>(cx)
1529                .unwrap()
1530                .read(cx)
1531                .picker
1532                .read(cx)
1533                .delegate
1534                .selected_index()
1535        });
1536        assert_eq!(
1537            selected_index, 0,
1538            "Should wrap around the history and start all over"
1539        );
1540    }
1541
1542    #[gpui::test]
1543    async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1544        let app_state = init_test(cx);
1545
1546        app_state
1547            .fs
1548            .as_fake()
1549            .insert_tree(
1550                "/src",
1551                json!({
1552                    "test": {
1553                        "first.rs": "// First Rust file",
1554                        "second.rs": "// Second Rust file",
1555                        "third.rs": "// Third Rust file",
1556                        "fourth.rs": "// Fourth Rust file",
1557                    }
1558                }),
1559            )
1560            .await;
1561
1562        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1563        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1564        let worktree_id = cx.read(|cx| {
1565            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1566            assert_eq!(worktrees.len(), 1,);
1567
1568            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1569        });
1570
1571        // generate some history to select from
1572        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1573        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1574        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1575        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1576
1577        let finder = open_file_picker(&workspace, cx);
1578        let first_query = "f";
1579        finder
1580            .update(cx, |finder, cx| {
1581                finder.delegate.update_matches(first_query.to_string(), cx)
1582            })
1583            .await;
1584        finder.update(cx, |finder, _| {
1585            let delegate = &finder.delegate;
1586            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1587            let history_match = delegate.matches.history.first().unwrap();
1588            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1589            assert_eq!(history_match.0, FoundPath::new(
1590                ProjectPath {
1591                    worktree_id,
1592                    path: Arc::from(Path::new("test/first.rs")),
1593                },
1594                Some(PathBuf::from("/src/test/first.rs"))
1595            ));
1596            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1597            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1598        });
1599
1600        let second_query = "fsdasdsa";
1601        let finder = active_file_picker(&workspace, cx);
1602        finder
1603            .update(cx, |finder, cx| {
1604                finder.delegate.update_matches(second_query.to_string(), cx)
1605            })
1606            .await;
1607        finder.update(cx, |finder, _| {
1608            let delegate = &finder.delegate;
1609            assert!(
1610                delegate.matches.history.is_empty(),
1611                "No history entries should match {second_query}"
1612            );
1613            assert!(
1614                delegate.matches.search.is_empty(),
1615                "No search entries should match {second_query}"
1616            );
1617        });
1618
1619        let first_query_again = first_query;
1620
1621        let finder = active_file_picker(&workspace, cx);
1622        finder
1623            .update(cx, |finder, cx| {
1624                finder
1625                    .delegate
1626                    .update_matches(first_query_again.to_string(), cx)
1627            })
1628            .await;
1629        finder.update(cx, |finder, _| {
1630            let delegate = &finder.delegate;
1631            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
1632            let history_match = delegate.matches.history.first().unwrap();
1633            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1634            assert_eq!(history_match.0, FoundPath::new(
1635                ProjectPath {
1636                    worktree_id,
1637                    path: Arc::from(Path::new("test/first.rs")),
1638                },
1639                Some(PathBuf::from("/src/test/first.rs"))
1640            ));
1641            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1642            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1643        });
1644    }
1645
1646    #[gpui::test]
1647    async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1648        let app_state = init_test(cx);
1649
1650        app_state
1651            .fs
1652            .as_fake()
1653            .insert_tree(
1654                "/src",
1655                json!({
1656                    "collab_ui": {
1657                        "first.rs": "// First Rust file",
1658                        "second.rs": "// Second Rust file",
1659                        "third.rs": "// Third Rust file",
1660                        "collab_ui.rs": "// Fourth Rust file",
1661                    }
1662                }),
1663            )
1664            .await;
1665
1666        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1667        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1668        // generate some history to select from
1669        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1670        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1671        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1672        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1673
1674        let finder = open_file_picker(&workspace, cx);
1675        let query = "collab_ui";
1676        cx.simulate_input(query);
1677        finder.update(cx, |finder, _| {
1678            let delegate = &finder.delegate;
1679            assert!(
1680                delegate.matches.history.is_empty(),
1681                "History items should not math query {query}, they should be matched by name only"
1682            );
1683
1684            let search_entries = delegate
1685                .matches
1686                .search
1687                .iter()
1688                .map(|path_match| path_match.path.to_path_buf())
1689                .collect::<Vec<_>>();
1690            assert_eq!(
1691                search_entries,
1692                vec![
1693                    PathBuf::from("collab_ui/collab_ui.rs"),
1694                    PathBuf::from("collab_ui/third.rs"),
1695                    PathBuf::from("collab_ui/first.rs"),
1696                    PathBuf::from("collab_ui/second.rs"),
1697                ],
1698                "Despite all search results having the same directory name, the most matching one should be on top"
1699            );
1700        });
1701    }
1702
1703    #[gpui::test]
1704    async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1705        let app_state = init_test(cx);
1706
1707        app_state
1708            .fs
1709            .as_fake()
1710            .insert_tree(
1711                "/src",
1712                json!({
1713                    "test": {
1714                        "first.rs": "// First Rust file",
1715                        "nonexistent.rs": "// Second Rust file",
1716                        "third.rs": "// Third Rust file",
1717                    }
1718                }),
1719            )
1720            .await;
1721
1722        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1723        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
1724        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1725        open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1726        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1727        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1728
1729        let picker = open_file_picker(&workspace, cx);
1730        cx.simulate_input("rs");
1731
1732        picker.update(cx, |finder, _| {
1733            let history_entries = finder.delegate
1734                .matches
1735                .history
1736                .iter()
1737                .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
1738                .collect::<Vec<_>>();
1739            assert_eq!(
1740                history_entries,
1741                vec![
1742                    PathBuf::from("test/first.rs"),
1743                    PathBuf::from("test/third.rs"),
1744                ],
1745                "Should have all opened files in the history, except the ones that do not exist on disk"
1746            );
1747        });
1748    }
1749
1750    async fn open_close_queried_buffer(
1751        input: &str,
1752        expected_matches: usize,
1753        expected_editor_title: &str,
1754        workspace: &View<Workspace>,
1755        cx: &mut gpui::VisualTestContext<'_>,
1756    ) -> Vec<FoundPath> {
1757        let picker = open_file_picker(&workspace, cx);
1758        cx.simulate_input(input);
1759
1760        let history_items = picker.update(cx, |finder, _| {
1761            assert_eq!(
1762                finder.delegate.matches.len(),
1763                expected_matches,
1764                "Unexpected number of matches found for query {input}"
1765            );
1766            finder.delegate.history_items.clone()
1767        });
1768
1769        cx.dispatch_action(SelectNext);
1770        cx.dispatch_action(Confirm);
1771
1772        cx.read(|cx| {
1773            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1774            let active_editor_title = active_editor.read(cx).title(cx);
1775            assert_eq!(
1776                expected_editor_title, active_editor_title,
1777                "Unexpected editor title for query {input}"
1778            );
1779        });
1780
1781        cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1782
1783        history_items
1784    }
1785
1786    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1787        cx.update(|cx| {
1788            let state = AppState::test(cx);
1789            theme::init(theme::LoadThemes::JustBase, cx);
1790            language::init(cx);
1791            super::init(cx);
1792            editor::init(cx);
1793            workspace::init_settings(cx);
1794            Project::init_settings(cx);
1795            state
1796        })
1797    }
1798
1799    fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1800        PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1801            Ok::<_, std::convert::Infallible>(FileSearchQuery {
1802                raw_query: test_str.to_owned(),
1803                file_query_end: if path_like_str == test_str {
1804                    None
1805                } else {
1806                    Some(path_like_str.len())
1807                },
1808            })
1809        })
1810        .unwrap()
1811    }
1812
1813    fn build_find_picker(
1814        project: Model<Project>,
1815        cx: &mut TestAppContext,
1816    ) -> (
1817        View<Picker<FileFinderDelegate>>,
1818        View<Workspace>,
1819        &mut VisualTestContext,
1820    ) {
1821        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1822        let picker = open_file_picker(&workspace, cx);
1823        (picker, workspace, cx)
1824    }
1825
1826    #[track_caller]
1827    fn open_file_picker(
1828        workspace: &View<Workspace>,
1829        cx: &mut VisualTestContext,
1830    ) -> View<Picker<FileFinderDelegate>> {
1831        cx.dispatch_action(Toggle);
1832        active_file_picker(workspace, cx)
1833    }
1834
1835    #[track_caller]
1836    fn active_file_picker(
1837        workspace: &View<Workspace>,
1838        cx: &mut VisualTestContext,
1839    ) -> View<Picker<FileFinderDelegate>> {
1840        workspace.update(cx, |workspace, cx| {
1841            workspace
1842                .active_modal::<FileFinder>(cx)
1843                .unwrap()
1844                .read(cx)
1845                .picker
1846                .clone()
1847        })
1848    }
1849}