file_finder.rs

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