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