file_finder_tests.rs

   1use std::{assert_eq, future::IntoFuture, path::Path, time::Duration};
   2
   3use super::*;
   4use editor::Editor;
   5use gpui::{Entity, TestAppContext, VisualTestContext};
   6use menu::{Confirm, SelectNext, SelectPrevious};
   7use project::{FS_WATCH_LATENCY, RemoveOptions};
   8use serde_json::json;
   9use util::path;
  10use workspace::{AppState, OpenOptions, ToggleFileFinder, Workspace};
  11
  12#[ctor::ctor]
  13fn init_logger() {
  14    if std::env::var("RUST_LOG").is_ok() {
  15        env_logger::init();
  16    }
  17}
  18
  19#[test]
  20fn test_path_elision() {
  21    #[track_caller]
  22    fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
  23        let mut path = path.to_owned();
  24        let slice = PathComponentSlice::new(&path);
  25        let matches = Vec::from_iter(matches);
  26        if let Some(range) = slice.elision_range(budget - 1, &matches) {
  27            path.replace_range(range, "");
  28        }
  29        assert_eq!(path, expected);
  30    }
  31
  32    // Simple cases, mostly to check that different path shapes are handled gracefully.
  33    check("p/a/b/c/d/", 6, [], "p/…/d/");
  34    check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
  35    check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
  36    check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
  37
  38    check("p/a/b/c/d", 5, [], "p/…/d");
  39    check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
  40    check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
  41    check("p/a/b/c/d", 7, [6], "p/…/c/d");
  42
  43    check("/p/a/b/c/d/", 7, [], "/p/…/d/");
  44    check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
  45    check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
  46    check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
  47
  48    // If the budget can't be met, no elision is done.
  49    check(
  50        "project/dir/child/grandchild",
  51        5,
  52        [],
  53        "project/dir/child/grandchild",
  54    );
  55
  56    // The longest unmatched segment is picked for elision.
  57    check(
  58        "project/one/two/X/three/sub",
  59        21,
  60        [16],
  61        "project/…/X/three/sub",
  62    );
  63
  64    // Elision stops when the budget is met, even though there are more components in the chosen segment.
  65    // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
  66    check(
  67        "project/one/two/three/X/sub",
  68        21,
  69        [22],
  70        "project/…/three/X/sub",
  71    )
  72}
  73
  74#[test]
  75fn test_custom_project_search_ordering_in_file_finder() {
  76    let mut file_finder_sorted_output = vec![
  77        ProjectPanelOrdMatch(PathMatch {
  78            score: 0.5,
  79            positions: Vec::new(),
  80            worktree_id: 0,
  81            path: Arc::from(Path::new("b0.5")),
  82            path_prefix: Arc::default(),
  83            distance_to_relative_ancestor: 0,
  84            is_dir: false,
  85        }),
  86        ProjectPanelOrdMatch(PathMatch {
  87            score: 1.0,
  88            positions: Vec::new(),
  89            worktree_id: 0,
  90            path: Arc::from(Path::new("c1.0")),
  91            path_prefix: Arc::default(),
  92            distance_to_relative_ancestor: 0,
  93            is_dir: false,
  94        }),
  95        ProjectPanelOrdMatch(PathMatch {
  96            score: 1.0,
  97            positions: Vec::new(),
  98            worktree_id: 0,
  99            path: Arc::from(Path::new("a1.0")),
 100            path_prefix: Arc::default(),
 101            distance_to_relative_ancestor: 0,
 102            is_dir: false,
 103        }),
 104        ProjectPanelOrdMatch(PathMatch {
 105            score: 0.5,
 106            positions: Vec::new(),
 107            worktree_id: 0,
 108            path: Arc::from(Path::new("a0.5")),
 109            path_prefix: Arc::default(),
 110            distance_to_relative_ancestor: 0,
 111            is_dir: false,
 112        }),
 113        ProjectPanelOrdMatch(PathMatch {
 114            score: 1.0,
 115            positions: Vec::new(),
 116            worktree_id: 0,
 117            path: Arc::from(Path::new("b1.0")),
 118            path_prefix: Arc::default(),
 119            distance_to_relative_ancestor: 0,
 120            is_dir: false,
 121        }),
 122    ];
 123    file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
 124
 125    assert_eq!(
 126        file_finder_sorted_output,
 127        vec![
 128            ProjectPanelOrdMatch(PathMatch {
 129                score: 1.0,
 130                positions: Vec::new(),
 131                worktree_id: 0,
 132                path: Arc::from(Path::new("a1.0")),
 133                path_prefix: Arc::default(),
 134                distance_to_relative_ancestor: 0,
 135                is_dir: false,
 136            }),
 137            ProjectPanelOrdMatch(PathMatch {
 138                score: 1.0,
 139                positions: Vec::new(),
 140                worktree_id: 0,
 141                path: Arc::from(Path::new("b1.0")),
 142                path_prefix: Arc::default(),
 143                distance_to_relative_ancestor: 0,
 144                is_dir: false,
 145            }),
 146            ProjectPanelOrdMatch(PathMatch {
 147                score: 1.0,
 148                positions: Vec::new(),
 149                worktree_id: 0,
 150                path: Arc::from(Path::new("c1.0")),
 151                path_prefix: Arc::default(),
 152                distance_to_relative_ancestor: 0,
 153                is_dir: false,
 154            }),
 155            ProjectPanelOrdMatch(PathMatch {
 156                score: 0.5,
 157                positions: Vec::new(),
 158                worktree_id: 0,
 159                path: Arc::from(Path::new("a0.5")),
 160                path_prefix: Arc::default(),
 161                distance_to_relative_ancestor: 0,
 162                is_dir: false,
 163            }),
 164            ProjectPanelOrdMatch(PathMatch {
 165                score: 0.5,
 166                positions: Vec::new(),
 167                worktree_id: 0,
 168                path: Arc::from(Path::new("b0.5")),
 169                path_prefix: Arc::default(),
 170                distance_to_relative_ancestor: 0,
 171                is_dir: false,
 172            }),
 173        ]
 174    );
 175}
 176
 177#[gpui::test]
 178async fn test_matching_paths(cx: &mut TestAppContext) {
 179    let app_state = init_test(cx);
 180    app_state
 181        .fs
 182        .as_fake()
 183        .insert_tree(
 184            path!("/root"),
 185            json!({
 186                "a": {
 187                    "banana": "",
 188                    "bandana": "",
 189                }
 190            }),
 191        )
 192        .await;
 193
 194    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 195
 196    let (picker, workspace, cx) = build_find_picker(project, cx);
 197
 198    cx.simulate_input("bna");
 199    picker.update(cx, |picker, _| {
 200        assert_eq!(picker.delegate.matches.len(), 2);
 201    });
 202    cx.dispatch_action(SelectNext);
 203    cx.dispatch_action(Confirm);
 204    cx.read(|cx| {
 205        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 206        assert_eq!(active_editor.read(cx).title(cx), "bandana");
 207    });
 208
 209    for bandana_query in [
 210        "bandana",
 211        " bandana",
 212        "bandana ",
 213        " bandana ",
 214        " ndan ",
 215        " band ",
 216        "a bandana",
 217    ] {
 218        picker
 219            .update_in(cx, |picker, window, cx| {
 220                picker
 221                    .delegate
 222                    .update_matches(bandana_query.to_string(), window, cx)
 223            })
 224            .await;
 225        picker.update(cx, |picker, _| {
 226            assert_eq!(
 227                picker.delegate.matches.len(),
 228                1,
 229                "Wrong number of matches for bandana query '{bandana_query}'"
 230            );
 231        });
 232        cx.dispatch_action(SelectNext);
 233        cx.dispatch_action(Confirm);
 234        cx.read(|cx| {
 235            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 236            assert_eq!(
 237                active_editor.read(cx).title(cx),
 238                "bandana",
 239                "Wrong match for bandana query '{bandana_query}'"
 240            );
 241        });
 242    }
 243}
 244
 245#[gpui::test]
 246async fn test_absolute_paths(cx: &mut TestAppContext) {
 247    let app_state = init_test(cx);
 248    app_state
 249        .fs
 250        .as_fake()
 251        .insert_tree(
 252            path!("/root"),
 253            json!({
 254                "a": {
 255                    "file1.txt": "",
 256                    "b": {
 257                        "file2.txt": "",
 258                    },
 259                }
 260            }),
 261        )
 262        .await;
 263
 264    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 265
 266    let (picker, workspace, cx) = build_find_picker(project, cx);
 267
 268    let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
 269    picker
 270        .update_in(cx, |picker, window, cx| {
 271            picker
 272                .delegate
 273                .update_matches(matching_abs_path, window, cx)
 274        })
 275        .await;
 276    picker.update(cx, |picker, _| {
 277        assert_eq!(
 278            collect_search_matches(picker).search_paths_only(),
 279            vec![PathBuf::from("a/b/file2.txt")],
 280            "Matching abs path should be the only match"
 281        )
 282    });
 283    cx.dispatch_action(SelectNext);
 284    cx.dispatch_action(Confirm);
 285    cx.read(|cx| {
 286        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 287        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
 288    });
 289
 290    let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
 291    picker
 292        .update_in(cx, |picker, window, cx| {
 293            picker
 294                .delegate
 295                .update_matches(mismatching_abs_path, window, cx)
 296        })
 297        .await;
 298    picker.update(cx, |picker, _| {
 299        assert_eq!(
 300            collect_search_matches(picker).search_paths_only(),
 301            Vec::<PathBuf>::new(),
 302            "Mismatching abs path should produce no matches"
 303        )
 304    });
 305}
 306
 307#[gpui::test]
 308async fn test_complex_path(cx: &mut TestAppContext) {
 309    let app_state = init_test(cx);
 310    app_state
 311        .fs
 312        .as_fake()
 313        .insert_tree(
 314            path!("/root"),
 315            json!({
 316                "其他": {
 317                    "S数据表格": {
 318                        "task.xlsx": "some content",
 319                    },
 320                }
 321            }),
 322        )
 323        .await;
 324
 325    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 326
 327    let (picker, workspace, cx) = build_find_picker(project, cx);
 328
 329    cx.simulate_input("t");
 330    picker.update(cx, |picker, _| {
 331        assert_eq!(picker.delegate.matches.len(), 1);
 332        assert_eq!(
 333            collect_search_matches(picker).search_paths_only(),
 334            vec![PathBuf::from("其他/S数据表格/task.xlsx")],
 335        )
 336    });
 337    cx.dispatch_action(SelectNext);
 338    cx.dispatch_action(Confirm);
 339    cx.read(|cx| {
 340        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 341        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
 342    });
 343}
 344
 345#[gpui::test]
 346async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 347    let app_state = init_test(cx);
 348
 349    let first_file_name = "first.rs";
 350    let first_file_contents = "// First Rust file";
 351    app_state
 352        .fs
 353        .as_fake()
 354        .insert_tree(
 355            path!("/src"),
 356            json!({
 357                "test": {
 358                    first_file_name: first_file_contents,
 359                    "second.rs": "// Second Rust file",
 360                }
 361            }),
 362        )
 363        .await;
 364
 365    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 366
 367    let (picker, workspace, cx) = build_find_picker(project, cx);
 368
 369    let file_query = &first_file_name[..3];
 370    let file_row = 1;
 371    let file_column = 3;
 372    assert!(file_column <= first_file_contents.len());
 373    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 374    picker
 375        .update_in(cx, |finder, window, cx| {
 376            finder
 377                .delegate
 378                .update_matches(query_inside_file.to_string(), window, cx)
 379        })
 380        .await;
 381    picker.update(cx, |finder, _| {
 382        let finder = &finder.delegate;
 383        assert_eq!(finder.matches.len(), 1);
 384        let latest_search_query = finder
 385            .latest_search_query
 386            .as_ref()
 387            .expect("Finder should have a query after the update_matches call");
 388        assert_eq!(latest_search_query.raw_query, query_inside_file);
 389        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 390        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 391        assert_eq!(
 392            latest_search_query.path_position.column,
 393            Some(file_column as u32)
 394        );
 395    });
 396
 397    cx.dispatch_action(SelectNext);
 398    cx.dispatch_action(Confirm);
 399
 400    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 401    cx.executor().advance_clock(Duration::from_secs(2));
 402
 403    editor.update(cx, |editor, cx| {
 404            let all_selections = editor.selections.all_adjusted(cx);
 405            assert_eq!(
 406                all_selections.len(),
 407                1,
 408                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 409            );
 410            let caret_selection = all_selections.into_iter().next().unwrap();
 411            assert_eq!(caret_selection.start, caret_selection.end,
 412                "Caret selection should have its start and end at the same position");
 413            assert_eq!(file_row, caret_selection.start.row + 1,
 414                "Query inside file should get caret with the same focus row");
 415            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 416                "Query inside file should get caret with the same focus column");
 417        });
 418}
 419
 420#[gpui::test]
 421async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 422    let app_state = init_test(cx);
 423
 424    let first_file_name = "first.rs";
 425    let first_file_contents = "// First Rust file";
 426    app_state
 427        .fs
 428        .as_fake()
 429        .insert_tree(
 430            path!("/src"),
 431            json!({
 432                "test": {
 433                    first_file_name: first_file_contents,
 434                    "second.rs": "// Second Rust file",
 435                }
 436            }),
 437        )
 438        .await;
 439
 440    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 441
 442    let (picker, workspace, cx) = build_find_picker(project, cx);
 443
 444    let file_query = &first_file_name[..3];
 445    let file_row = 200;
 446    let file_column = 300;
 447    assert!(file_column > first_file_contents.len());
 448    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 449    picker
 450        .update_in(cx, |picker, window, cx| {
 451            picker
 452                .delegate
 453                .update_matches(query_outside_file.to_string(), window, cx)
 454        })
 455        .await;
 456    picker.update(cx, |finder, _| {
 457        let delegate = &finder.delegate;
 458        assert_eq!(delegate.matches.len(), 1);
 459        let latest_search_query = delegate
 460            .latest_search_query
 461            .as_ref()
 462            .expect("Finder should have a query after the update_matches call");
 463        assert_eq!(latest_search_query.raw_query, query_outside_file);
 464        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 465        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 466        assert_eq!(
 467            latest_search_query.path_position.column,
 468            Some(file_column as u32)
 469        );
 470    });
 471
 472    cx.dispatch_action(SelectNext);
 473    cx.dispatch_action(Confirm);
 474
 475    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 476    cx.executor().advance_clock(Duration::from_secs(2));
 477
 478    editor.update(cx, |editor, cx| {
 479            let all_selections = editor.selections.all_adjusted(cx);
 480            assert_eq!(
 481                all_selections.len(),
 482                1,
 483                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 484            );
 485            let caret_selection = all_selections.into_iter().next().unwrap();
 486            assert_eq!(caret_selection.start, caret_selection.end,
 487                "Caret selection should have its start and end at the same position");
 488            assert_eq!(0, caret_selection.start.row,
 489                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 490            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 491                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 492        });
 493}
 494
 495#[gpui::test]
 496async fn test_matching_cancellation(cx: &mut TestAppContext) {
 497    let app_state = init_test(cx);
 498    app_state
 499        .fs
 500        .as_fake()
 501        .insert_tree(
 502            "/dir",
 503            json!({
 504                "hello": "",
 505                "goodbye": "",
 506                "halogen-light": "",
 507                "happiness": "",
 508                "height": "",
 509                "hi": "",
 510                "hiccup": "",
 511            }),
 512        )
 513        .await;
 514
 515    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 516
 517    let (picker, _, cx) = build_find_picker(project, cx);
 518
 519    let query = test_path_position("hi");
 520    picker
 521        .update_in(cx, |picker, window, cx| {
 522            picker.delegate.spawn_search(query.clone(), window, cx)
 523        })
 524        .await;
 525
 526    picker.update(cx, |picker, _cx| {
 527        assert_eq!(picker.delegate.matches.len(), 5)
 528    });
 529
 530    picker.update_in(cx, |picker, window, cx| {
 531        let matches = collect_search_matches(picker).search_matches_only();
 532        let delegate = &mut picker.delegate;
 533
 534        // Simulate a search being cancelled after the time limit,
 535        // returning only a subset of the matches that would have been found.
 536        drop(delegate.spawn_search(query.clone(), window, cx));
 537        delegate.set_search_matches(
 538            delegate.latest_search_id,
 539            true, // did-cancel
 540            query.clone(),
 541            vec![
 542                ProjectPanelOrdMatch(matches[1].clone()),
 543                ProjectPanelOrdMatch(matches[3].clone()),
 544            ],
 545            cx,
 546        );
 547
 548        // Simulate another cancellation.
 549        drop(delegate.spawn_search(query.clone(), window, cx));
 550        delegate.set_search_matches(
 551            delegate.latest_search_id,
 552            true, // did-cancel
 553            query.clone(),
 554            vec![
 555                ProjectPanelOrdMatch(matches[0].clone()),
 556                ProjectPanelOrdMatch(matches[2].clone()),
 557                ProjectPanelOrdMatch(matches[3].clone()),
 558            ],
 559            cx,
 560        );
 561
 562        assert_eq!(
 563            collect_search_matches(picker)
 564                .search_matches_only()
 565                .as_slice(),
 566            &matches[0..4]
 567        );
 568    });
 569}
 570
 571#[gpui::test]
 572async fn test_ignored_root(cx: &mut TestAppContext) {
 573    let app_state = init_test(cx);
 574    app_state
 575        .fs
 576        .as_fake()
 577        .insert_tree(
 578            "/ancestor",
 579            json!({
 580                ".gitignore": "ignored-root",
 581                "ignored-root": {
 582                    "happiness": "",
 583                    "height": "",
 584                    "hi": "",
 585                    "hiccup": "",
 586                },
 587                "tracked-root": {
 588                    ".gitignore": "height",
 589                    "happiness": "",
 590                    "height": "",
 591                    "hi": "",
 592                    "hiccup": "",
 593                },
 594            }),
 595        )
 596        .await;
 597
 598    let project = Project::test(
 599        app_state.fs.clone(),
 600        [
 601            "/ancestor/tracked-root".as_ref(),
 602            "/ancestor/ignored-root".as_ref(),
 603        ],
 604        cx,
 605    )
 606    .await;
 607
 608    let (picker, _, cx) = build_find_picker(project, cx);
 609
 610    picker
 611        .update_in(cx, |picker, window, cx| {
 612            picker
 613                .delegate
 614                .spawn_search(test_path_position("hi"), window, cx)
 615        })
 616        .await;
 617    picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
 618}
 619
 620#[gpui::test]
 621async fn test_single_file_worktrees(cx: &mut TestAppContext) {
 622    let app_state = init_test(cx);
 623    app_state
 624        .fs
 625        .as_fake()
 626        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
 627        .await;
 628
 629    let project = Project::test(
 630        app_state.fs.clone(),
 631        ["/root/the-parent-dir/the-file".as_ref()],
 632        cx,
 633    )
 634    .await;
 635
 636    let (picker, _, cx) = build_find_picker(project, cx);
 637
 638    // Even though there is only one worktree, that worktree's filename
 639    // is included in the matching, because the worktree is a single file.
 640    picker
 641        .update_in(cx, |picker, window, cx| {
 642            picker
 643                .delegate
 644                .spawn_search(test_path_position("thf"), window, cx)
 645        })
 646        .await;
 647    cx.read(|cx| {
 648        let picker = picker.read(cx);
 649        let delegate = &picker.delegate;
 650        let matches = collect_search_matches(picker).search_matches_only();
 651        assert_eq!(matches.len(), 1);
 652
 653        let (file_name, file_name_positions, full_path, full_path_positions) =
 654            delegate.labels_for_path_match(&matches[0]);
 655        assert_eq!(file_name, "the-file");
 656        assert_eq!(file_name_positions, &[0, 1, 4]);
 657        assert_eq!(full_path, "");
 658        assert_eq!(full_path_positions, &[0; 0]);
 659    });
 660
 661    // Since the worktree root is a file, searching for its name followed by a slash does
 662    // not match anything.
 663    picker
 664        .update_in(cx, |picker, window, cx| {
 665            picker
 666                .delegate
 667                .spawn_search(test_path_position("thf/"), window, cx)
 668        })
 669        .await;
 670    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
 671}
 672
 673#[gpui::test]
 674async fn test_path_distance_ordering(cx: &mut TestAppContext) {
 675    let app_state = init_test(cx);
 676    app_state
 677        .fs
 678        .as_fake()
 679        .insert_tree(
 680            path!("/root"),
 681            json!({
 682                "dir1": { "a.txt": "" },
 683                "dir2": {
 684                    "a.txt": "",
 685                    "b.txt": ""
 686                }
 687            }),
 688        )
 689        .await;
 690
 691    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 692    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 693
 694    let worktree_id = cx.read(|cx| {
 695        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 696        assert_eq!(worktrees.len(), 1);
 697        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 698    });
 699
 700    // When workspace has an active item, sort items which are closer to that item
 701    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
 702    // so that one should be sorted earlier
 703    let b_path = ProjectPath {
 704        worktree_id,
 705        path: Arc::from(Path::new("dir2/b.txt")),
 706    };
 707    workspace
 708        .update_in(cx, |workspace, window, cx| {
 709            workspace.open_path(b_path, None, true, window, cx)
 710        })
 711        .await
 712        .unwrap();
 713    let finder = open_file_picker(&workspace, cx);
 714    finder
 715        .update_in(cx, |f, window, cx| {
 716            f.delegate
 717                .spawn_search(test_path_position("a.txt"), window, cx)
 718        })
 719        .await;
 720
 721    finder.update(cx, |picker, _| {
 722        let matches = collect_search_matches(picker).search_paths_only();
 723        assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
 724        assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
 725    });
 726}
 727
 728#[gpui::test]
 729async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
 730    let app_state = init_test(cx);
 731    app_state
 732        .fs
 733        .as_fake()
 734        .insert_tree(
 735            "/root",
 736            json!({
 737                "dir1": {},
 738                "dir2": {
 739                    "dir3": {}
 740                }
 741            }),
 742        )
 743        .await;
 744
 745    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 746    let (picker, _workspace, cx) = build_find_picker(project, cx);
 747
 748    picker
 749        .update_in(cx, |f, window, cx| {
 750            f.delegate
 751                .spawn_search(test_path_position("dir"), window, cx)
 752        })
 753        .await;
 754    cx.read(|cx| {
 755        let finder = picker.read(cx);
 756        assert_eq!(finder.delegate.matches.len(), 0);
 757    });
 758}
 759
 760#[gpui::test]
 761async fn test_query_history(cx: &mut gpui::TestAppContext) {
 762    let app_state = init_test(cx);
 763
 764    app_state
 765        .fs
 766        .as_fake()
 767        .insert_tree(
 768            path!("/src"),
 769            json!({
 770                "test": {
 771                    "first.rs": "// First Rust file",
 772                    "second.rs": "// Second Rust file",
 773                    "third.rs": "// Third Rust file",
 774                }
 775            }),
 776        )
 777        .await;
 778
 779    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 780    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 781    let worktree_id = cx.read(|cx| {
 782        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 783        assert_eq!(worktrees.len(), 1);
 784        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 785    });
 786
 787    // Open and close panels, getting their history items afterwards.
 788    // Ensure history items get populated with opened items, and items are kept in a certain order.
 789    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
 790    //
 791    // TODO: without closing, the opened items do not propagate their history changes for some reason
 792    // it does work in real app though, only tests do not propagate.
 793    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
 794
 795    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 796    assert!(
 797        initial_history.is_empty(),
 798        "Should have no history before opening any files"
 799    );
 800
 801    let history_after_first =
 802        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 803    assert_eq!(
 804        history_after_first,
 805        vec![FoundPath::new(
 806            ProjectPath {
 807                worktree_id,
 808                path: Arc::from(Path::new("test/first.rs")),
 809            },
 810            Some(PathBuf::from(path!("/src/test/first.rs")))
 811        )],
 812        "Should show 1st opened item in the history when opening the 2nd item"
 813    );
 814
 815    let history_after_second =
 816        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 817    assert_eq!(
 818        history_after_second,
 819        vec![
 820            FoundPath::new(
 821                ProjectPath {
 822                    worktree_id,
 823                    path: Arc::from(Path::new("test/second.rs")),
 824                },
 825                Some(PathBuf::from(path!("/src/test/second.rs")))
 826            ),
 827            FoundPath::new(
 828                ProjectPath {
 829                    worktree_id,
 830                    path: Arc::from(Path::new("test/first.rs")),
 831                },
 832                Some(PathBuf::from(path!("/src/test/first.rs")))
 833            ),
 834        ],
 835        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
 836    2nd item should be the first in the history, as the last opened."
 837    );
 838
 839    let history_after_third =
 840        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 841    assert_eq!(
 842        history_after_third,
 843        vec![
 844            FoundPath::new(
 845                ProjectPath {
 846                    worktree_id,
 847                    path: Arc::from(Path::new("test/third.rs")),
 848                },
 849                Some(PathBuf::from(path!("/src/test/third.rs")))
 850            ),
 851            FoundPath::new(
 852                ProjectPath {
 853                    worktree_id,
 854                    path: Arc::from(Path::new("test/second.rs")),
 855                },
 856                Some(PathBuf::from(path!("/src/test/second.rs")))
 857            ),
 858            FoundPath::new(
 859                ProjectPath {
 860                    worktree_id,
 861                    path: Arc::from(Path::new("test/first.rs")),
 862                },
 863                Some(PathBuf::from(path!("/src/test/first.rs")))
 864            ),
 865        ],
 866        "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
 867    3rd item should be the first in the history, as the last opened."
 868    );
 869
 870    let history_after_second_again =
 871        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 872    assert_eq!(
 873        history_after_second_again,
 874        vec![
 875            FoundPath::new(
 876                ProjectPath {
 877                    worktree_id,
 878                    path: Arc::from(Path::new("test/second.rs")),
 879                },
 880                Some(PathBuf::from(path!("/src/test/second.rs")))
 881            ),
 882            FoundPath::new(
 883                ProjectPath {
 884                    worktree_id,
 885                    path: Arc::from(Path::new("test/third.rs")),
 886                },
 887                Some(PathBuf::from(path!("/src/test/third.rs")))
 888            ),
 889            FoundPath::new(
 890                ProjectPath {
 891                    worktree_id,
 892                    path: Arc::from(Path::new("test/first.rs")),
 893                },
 894                Some(PathBuf::from(path!("/src/test/first.rs")))
 895            ),
 896        ],
 897        "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
 898    2nd item, as the last opened, 3rd item should go next as it was opened right before."
 899    );
 900}
 901
 902#[gpui::test]
 903async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
 904    let app_state = init_test(cx);
 905
 906    app_state
 907        .fs
 908        .as_fake()
 909        .insert_tree(
 910            path!("/src"),
 911            json!({
 912                "test": {
 913                    "first.rs": "// First Rust file",
 914                    "second.rs": "// Second Rust file",
 915                }
 916            }),
 917        )
 918        .await;
 919
 920    app_state
 921        .fs
 922        .as_fake()
 923        .insert_tree(
 924            path!("/external-src"),
 925            json!({
 926                "test": {
 927                    "third.rs": "// Third Rust file",
 928                    "fourth.rs": "// Fourth Rust file",
 929                }
 930            }),
 931        )
 932        .await;
 933
 934    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 935    cx.update(|cx| {
 936        project.update(cx, |project, cx| {
 937            project.find_or_create_worktree(path!("/external-src"), false, cx)
 938        })
 939    })
 940    .detach();
 941    cx.background_executor.run_until_parked();
 942
 943    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 944    let worktree_id = cx.read(|cx| {
 945        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 946        assert_eq!(worktrees.len(), 1,);
 947
 948        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 949    });
 950    workspace
 951        .update_in(cx, |workspace, window, cx| {
 952            workspace.open_abs_path(
 953                PathBuf::from(path!("/external-src/test/third.rs")),
 954                OpenOptions {
 955                    visible: Some(OpenVisible::None),
 956                    ..Default::default()
 957                },
 958                window,
 959                cx,
 960            )
 961        })
 962        .detach();
 963    cx.background_executor.run_until_parked();
 964    let external_worktree_id = cx.read(|cx| {
 965        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 966        assert_eq!(
 967            worktrees.len(),
 968            2,
 969            "External file should get opened in a new worktree"
 970        );
 971
 972        WorktreeId::from_usize(
 973            worktrees
 974                .into_iter()
 975                .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
 976                .expect("New worktree should have a different id")
 977                .entity_id()
 978                .as_u64() as usize,
 979        )
 980    });
 981    cx.dispatch_action(workspace::CloseActiveItem {
 982        save_intent: None,
 983        close_pinned: false,
 984    });
 985
 986    let initial_history_items =
 987        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 988    assert_eq!(
 989        initial_history_items,
 990        vec![FoundPath::new(
 991            ProjectPath {
 992                worktree_id: external_worktree_id,
 993                path: Arc::from(Path::new("")),
 994            },
 995            Some(PathBuf::from(path!("/external-src/test/third.rs")))
 996        )],
 997        "Should show external file with its full path in the history after it was open"
 998    );
 999
1000    let updated_history_items =
1001        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1002    assert_eq!(
1003        updated_history_items,
1004        vec![
1005            FoundPath::new(
1006                ProjectPath {
1007                    worktree_id,
1008                    path: Arc::from(Path::new("test/second.rs")),
1009                },
1010                Some(PathBuf::from(path!("/src/test/second.rs")))
1011            ),
1012            FoundPath::new(
1013                ProjectPath {
1014                    worktree_id: external_worktree_id,
1015                    path: Arc::from(Path::new("")),
1016                },
1017                Some(PathBuf::from(path!("/external-src/test/third.rs")))
1018            ),
1019        ],
1020        "Should keep external file with history updates",
1021    );
1022}
1023
1024#[gpui::test]
1025async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1026    let app_state = init_test(cx);
1027
1028    app_state
1029        .fs
1030        .as_fake()
1031        .insert_tree(
1032            path!("/src"),
1033            json!({
1034                "test": {
1035                    "first.rs": "// First Rust file",
1036                    "second.rs": "// Second Rust file",
1037                    "third.rs": "// Third Rust file",
1038                }
1039            }),
1040        )
1041        .await;
1042
1043    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1044    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1045
1046    // generate some history to select from
1047    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1048    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1049    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1050    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1051
1052    for expected_selected_index in 0..current_history.len() {
1053        cx.dispatch_action(ToggleFileFinder::default());
1054        let picker = active_file_picker(&workspace, cx);
1055        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1056        assert_eq!(
1057            selected_index, expected_selected_index,
1058            "Should select the next item in the history"
1059        );
1060    }
1061
1062    cx.dispatch_action(ToggleFileFinder::default());
1063    let selected_index = workspace.update(cx, |workspace, cx| {
1064        workspace
1065            .active_modal::<FileFinder>(cx)
1066            .unwrap()
1067            .read(cx)
1068            .picker
1069            .read(cx)
1070            .delegate
1071            .selected_index()
1072    });
1073    assert_eq!(
1074        selected_index, 0,
1075        "Should wrap around the history and start all over"
1076    );
1077}
1078
1079#[gpui::test]
1080async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1081    let app_state = init_test(cx);
1082
1083    app_state
1084        .fs
1085        .as_fake()
1086        .insert_tree(
1087            path!("/src"),
1088            json!({
1089                "test": {
1090                    "first.rs": "// First Rust file",
1091                    "second.rs": "// Second Rust file",
1092                    "third.rs": "// Third Rust file",
1093                    "fourth.rs": "// Fourth Rust file",
1094                }
1095            }),
1096        )
1097        .await;
1098
1099    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1100    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1101    let worktree_id = cx.read(|cx| {
1102        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1103        assert_eq!(worktrees.len(), 1,);
1104
1105        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1106    });
1107
1108    // generate some history to select from
1109    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1110    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1111    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1112    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1113
1114    let finder = open_file_picker(&workspace, cx);
1115    let first_query = "f";
1116    finder
1117        .update_in(cx, |finder, window, cx| {
1118            finder
1119                .delegate
1120                .update_matches(first_query.to_string(), window, cx)
1121        })
1122        .await;
1123    finder.update(cx, |picker, _| {
1124            let matches = collect_search_matches(picker);
1125            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1126            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1127            assert_eq!(history_match, &FoundPath::new(
1128                ProjectPath {
1129                    worktree_id,
1130                    path: Arc::from(Path::new("test/first.rs")),
1131                },
1132                Some(PathBuf::from(path!("/src/test/first.rs")))
1133            ));
1134            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1135            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
1136        });
1137
1138    let second_query = "fsdasdsa";
1139    let finder = active_file_picker(&workspace, cx);
1140    finder
1141        .update_in(cx, |finder, window, cx| {
1142            finder
1143                .delegate
1144                .update_matches(second_query.to_string(), window, cx)
1145        })
1146        .await;
1147    finder.update(cx, |picker, _| {
1148        assert!(
1149            collect_search_matches(picker)
1150                .search_paths_only()
1151                .is_empty(),
1152            "No search entries should match {second_query}"
1153        );
1154    });
1155
1156    let first_query_again = first_query;
1157
1158    let finder = active_file_picker(&workspace, cx);
1159    finder
1160        .update_in(cx, |finder, window, cx| {
1161            finder
1162                .delegate
1163                .update_matches(first_query_again.to_string(), window, cx)
1164        })
1165        .await;
1166    finder.update(cx, |picker, _| {
1167            let matches = collect_search_matches(picker);
1168            assert_eq!(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");
1169            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1170            assert_eq!(history_match, &FoundPath::new(
1171                ProjectPath {
1172                    worktree_id,
1173                    path: Arc::from(Path::new("test/first.rs")),
1174                },
1175                Some(PathBuf::from(path!("/src/test/first.rs")))
1176            ));
1177            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1178            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
1179        });
1180}
1181
1182#[gpui::test]
1183async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1184    let app_state = init_test(cx);
1185
1186    app_state
1187        .fs
1188        .as_fake()
1189        .insert_tree(
1190            path!("/root"),
1191            json!({
1192                "test": {
1193                    "1_qw": "// First file that matches the query",
1194                    "2_second": "// Second file",
1195                    "3_third": "// Third file",
1196                    "4_fourth": "// Fourth file",
1197                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1198                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1199                    "7_qwqwqw": "// One more, same amount of query matches as above",
1200                }
1201            }),
1202        )
1203        .await;
1204
1205    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1206    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1207    // generate some history to select from
1208    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1209    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1210    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1211    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1212    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1213
1214    let finder = open_file_picker(&workspace, cx);
1215    let query = "qw";
1216    finder
1217        .update_in(cx, |finder, window, cx| {
1218            finder
1219                .delegate
1220                .update_matches(query.to_string(), window, cx)
1221        })
1222        .await;
1223    finder.update(cx, |finder, _| {
1224        let search_matches = collect_search_matches(finder);
1225        assert_eq!(
1226            search_matches.history,
1227            vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1228        );
1229        assert_eq!(
1230            search_matches.search,
1231            vec![
1232                PathBuf::from("test/5_qwqwqw"),
1233                PathBuf::from("test/7_qwqwqw"),
1234            ],
1235        );
1236    });
1237}
1238
1239#[gpui::test]
1240async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1241    let app_state = init_test(cx);
1242
1243    app_state
1244        .fs
1245        .as_fake()
1246        .insert_tree(
1247            path!("/root"),
1248            json!({
1249                "test": {
1250                    "1_qw": "",
1251                }
1252            }),
1253        )
1254        .await;
1255
1256    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1257    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1258    // Open new buffer
1259    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1260
1261    let picker = open_file_picker(&workspace, cx);
1262    picker.update(cx, |finder, _| {
1263        assert_match_selection(&finder, 0, "1_qw");
1264    });
1265}
1266
1267#[gpui::test]
1268async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1269    cx: &mut TestAppContext,
1270) {
1271    let app_state = init_test(cx);
1272
1273    app_state
1274        .fs
1275        .as_fake()
1276        .insert_tree(
1277            path!("/src"),
1278            json!({
1279                "test": {
1280                    "bar.rs": "// Bar file",
1281                    "lib.rs": "// Lib file",
1282                    "maaa.rs": "// Maaaaaaa",
1283                    "main.rs": "// Main file",
1284                    "moo.rs": "// Moooooo",
1285                }
1286            }),
1287        )
1288        .await;
1289
1290    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1291    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1292
1293    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1294    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1295    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1296
1297    // main.rs is on top, previously used is selected
1298    let picker = open_file_picker(&workspace, cx);
1299    picker.update(cx, |finder, _| {
1300        assert_eq!(finder.delegate.matches.len(), 3);
1301        assert_match_selection(finder, 0, "main.rs");
1302        assert_match_at_position(finder, 1, "lib.rs");
1303        assert_match_at_position(finder, 2, "bar.rs");
1304    });
1305
1306    // all files match, main.rs is still on top, but the second item is selected
1307    picker
1308        .update_in(cx, |finder, window, cx| {
1309            finder
1310                .delegate
1311                .update_matches(".rs".to_string(), window, cx)
1312        })
1313        .await;
1314    picker.update(cx, |finder, _| {
1315        assert_eq!(finder.delegate.matches.len(), 5);
1316        assert_match_at_position(finder, 0, "main.rs");
1317        assert_match_selection(finder, 1, "bar.rs");
1318        assert_match_at_position(finder, 2, "lib.rs");
1319        assert_match_at_position(finder, 3, "moo.rs");
1320        assert_match_at_position(finder, 4, "maaa.rs");
1321    });
1322
1323    // main.rs is not among matches, select top item
1324    picker
1325        .update_in(cx, |finder, window, cx| {
1326            finder.delegate.update_matches("b".to_string(), window, cx)
1327        })
1328        .await;
1329    picker.update(cx, |finder, _| {
1330        assert_eq!(finder.delegate.matches.len(), 2);
1331        assert_match_at_position(finder, 0, "bar.rs");
1332        assert_match_at_position(finder, 1, "lib.rs");
1333    });
1334
1335    // main.rs is back, put it on top and select next item
1336    picker
1337        .update_in(cx, |finder, window, cx| {
1338            finder.delegate.update_matches("m".to_string(), window, cx)
1339        })
1340        .await;
1341    picker.update(cx, |finder, _| {
1342        assert_eq!(finder.delegate.matches.len(), 3);
1343        assert_match_at_position(finder, 0, "main.rs");
1344        assert_match_selection(finder, 1, "moo.rs");
1345        assert_match_at_position(finder, 2, "maaa.rs");
1346    });
1347
1348    // get back to the initial state
1349    picker
1350        .update_in(cx, |finder, window, cx| {
1351            finder.delegate.update_matches("".to_string(), window, cx)
1352        })
1353        .await;
1354    picker.update(cx, |finder, _| {
1355        assert_eq!(finder.delegate.matches.len(), 3);
1356        assert_match_selection(finder, 0, "main.rs");
1357        assert_match_at_position(finder, 1, "lib.rs");
1358        assert_match_at_position(finder, 2, "bar.rs");
1359    });
1360}
1361
1362#[gpui::test]
1363async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
1364    let app_state = init_test(cx);
1365
1366    cx.update(|cx| {
1367        let settings = *FileFinderSettings::get_global(cx);
1368
1369        FileFinderSettings::override_global(
1370            FileFinderSettings {
1371                skip_focus_for_active_in_search: false,
1372                ..settings
1373            },
1374            cx,
1375        );
1376    });
1377
1378    app_state
1379        .fs
1380        .as_fake()
1381        .insert_tree(
1382            path!("/src"),
1383            json!({
1384                "test": {
1385                    "bar.rs": "// Bar file",
1386                    "lib.rs": "// Lib file",
1387                    "maaa.rs": "// Maaaaaaa",
1388                    "main.rs": "// Main file",
1389                    "moo.rs": "// Moooooo",
1390                }
1391            }),
1392        )
1393        .await;
1394
1395    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1396    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1397
1398    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1399    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1400    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1401
1402    // main.rs is on top, previously used is selected
1403    let picker = open_file_picker(&workspace, cx);
1404    picker.update(cx, |finder, _| {
1405        assert_eq!(finder.delegate.matches.len(), 3);
1406        assert_match_selection(finder, 0, "main.rs");
1407        assert_match_at_position(finder, 1, "lib.rs");
1408        assert_match_at_position(finder, 2, "bar.rs");
1409    });
1410
1411    // all files match, main.rs is on top, and is selected
1412    picker
1413        .update_in(cx, |finder, window, cx| {
1414            finder
1415                .delegate
1416                .update_matches(".rs".to_string(), window, cx)
1417        })
1418        .await;
1419    picker.update(cx, |finder, _| {
1420        assert_eq!(finder.delegate.matches.len(), 5);
1421        assert_match_selection(finder, 0, "main.rs");
1422        assert_match_at_position(finder, 1, "bar.rs");
1423        assert_match_at_position(finder, 2, "lib.rs");
1424        assert_match_at_position(finder, 3, "moo.rs");
1425        assert_match_at_position(finder, 4, "maaa.rs");
1426    });
1427}
1428
1429#[gpui::test]
1430async fn test_non_separate_history_items(cx: &mut TestAppContext) {
1431    let app_state = init_test(cx);
1432
1433    app_state
1434        .fs
1435        .as_fake()
1436        .insert_tree(
1437            path!("/src"),
1438            json!({
1439                "test": {
1440                    "bar.rs": "// Bar file",
1441                    "lib.rs": "// Lib file",
1442                    "maaa.rs": "// Maaaaaaa",
1443                    "main.rs": "// Main file",
1444                    "moo.rs": "// Moooooo",
1445                }
1446            }),
1447        )
1448        .await;
1449
1450    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1451    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1452
1453    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1454    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1455    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1456
1457    cx.dispatch_action(ToggleFileFinder::default());
1458    let picker = active_file_picker(&workspace, cx);
1459    // main.rs is on top, previously used is selected
1460    picker.update(cx, |finder, _| {
1461        assert_eq!(finder.delegate.matches.len(), 3);
1462        assert_match_selection(finder, 0, "main.rs");
1463        assert_match_at_position(finder, 1, "lib.rs");
1464        assert_match_at_position(finder, 2, "bar.rs");
1465    });
1466
1467    // all files match, main.rs is still on top, but the second item is selected
1468    picker
1469        .update_in(cx, |finder, window, cx| {
1470            finder
1471                .delegate
1472                .update_matches(".rs".to_string(), window, cx)
1473        })
1474        .await;
1475    picker.update(cx, |finder, _| {
1476        assert_eq!(finder.delegate.matches.len(), 5);
1477        assert_match_at_position(finder, 0, "main.rs");
1478        assert_match_selection(finder, 1, "moo.rs");
1479        assert_match_at_position(finder, 2, "bar.rs");
1480        assert_match_at_position(finder, 3, "lib.rs");
1481        assert_match_at_position(finder, 4, "maaa.rs");
1482    });
1483
1484    // main.rs is not among matches, select top item
1485    picker
1486        .update_in(cx, |finder, window, cx| {
1487            finder.delegate.update_matches("b".to_string(), window, cx)
1488        })
1489        .await;
1490    picker.update(cx, |finder, _| {
1491        assert_eq!(finder.delegate.matches.len(), 2);
1492        assert_match_at_position(finder, 0, "bar.rs");
1493        assert_match_at_position(finder, 1, "lib.rs");
1494    });
1495
1496    // main.rs is back, put it on top and select next item
1497    picker
1498        .update_in(cx, |finder, window, cx| {
1499            finder.delegate.update_matches("m".to_string(), window, cx)
1500        })
1501        .await;
1502    picker.update(cx, |finder, _| {
1503        assert_eq!(finder.delegate.matches.len(), 3);
1504        assert_match_at_position(finder, 0, "main.rs");
1505        assert_match_selection(finder, 1, "moo.rs");
1506        assert_match_at_position(finder, 2, "maaa.rs");
1507    });
1508
1509    // get back to the initial state
1510    picker
1511        .update_in(cx, |finder, window, cx| {
1512            finder.delegate.update_matches("".to_string(), window, cx)
1513        })
1514        .await;
1515    picker.update(cx, |finder, _| {
1516        assert_eq!(finder.delegate.matches.len(), 3);
1517        assert_match_selection(finder, 0, "main.rs");
1518        assert_match_at_position(finder, 1, "lib.rs");
1519        assert_match_at_position(finder, 2, "bar.rs");
1520    });
1521}
1522
1523#[gpui::test]
1524async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1525    let app_state = init_test(cx);
1526
1527    app_state
1528        .fs
1529        .as_fake()
1530        .insert_tree(
1531            path!("/test"),
1532            json!({
1533                "test": {
1534                    "1.txt": "// One",
1535                    "2.txt": "// Two",
1536                    "3.txt": "// Three",
1537                }
1538            }),
1539        )
1540        .await;
1541
1542    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1543    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1544
1545    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1546    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1547    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1548
1549    let picker = open_file_picker(&workspace, cx);
1550    picker.update(cx, |finder, _| {
1551        assert_eq!(finder.delegate.matches.len(), 3);
1552        assert_match_selection(finder, 0, "3.txt");
1553        assert_match_at_position(finder, 1, "2.txt");
1554        assert_match_at_position(finder, 2, "1.txt");
1555    });
1556
1557    cx.dispatch_action(SelectNext);
1558    cx.dispatch_action(Confirm); // Open 2.txt
1559
1560    let picker = open_file_picker(&workspace, cx);
1561    picker.update(cx, |finder, _| {
1562        assert_eq!(finder.delegate.matches.len(), 3);
1563        assert_match_selection(finder, 0, "2.txt");
1564        assert_match_at_position(finder, 1, "3.txt");
1565        assert_match_at_position(finder, 2, "1.txt");
1566    });
1567
1568    cx.dispatch_action(SelectNext);
1569    cx.dispatch_action(SelectNext);
1570    cx.dispatch_action(Confirm); // Open 1.txt
1571
1572    let picker = open_file_picker(&workspace, cx);
1573    picker.update(cx, |finder, _| {
1574        assert_eq!(finder.delegate.matches.len(), 3);
1575        assert_match_selection(finder, 0, "1.txt");
1576        assert_match_at_position(finder, 1, "2.txt");
1577        assert_match_at_position(finder, 2, "3.txt");
1578    });
1579}
1580
1581#[gpui::test]
1582async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
1583    let app_state = init_test(cx);
1584
1585    app_state
1586        .fs
1587        .as_fake()
1588        .insert_tree(
1589            path!("/test"),
1590            json!({
1591                "test": {
1592                    "1.txt": "// One",
1593                    "2.txt": "// Two",
1594                    "3.txt": "// Three",
1595                }
1596            }),
1597        )
1598        .await;
1599
1600    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1601    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1602
1603    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1604    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1605    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1606
1607    let picker = open_file_picker(&workspace, cx);
1608    picker.update(cx, |finder, _| {
1609        assert_eq!(finder.delegate.matches.len(), 3);
1610        assert_match_selection(finder, 0, "3.txt");
1611        assert_match_at_position(finder, 1, "2.txt");
1612        assert_match_at_position(finder, 2, "1.txt");
1613    });
1614
1615    cx.dispatch_action(SelectNext);
1616
1617    // Add more files to the worktree to trigger update matches
1618    for i in 0..5 {
1619        let filename = if cfg!(windows) {
1620            format!("C:/test/{}.txt", 4 + i)
1621        } else {
1622            format!("/test/{}.txt", 4 + i)
1623        };
1624        app_state
1625            .fs
1626            .create_file(Path::new(&filename), Default::default())
1627            .await
1628            .expect("unable to create file");
1629    }
1630
1631    cx.executor().advance_clock(FS_WATCH_LATENCY);
1632
1633    picker.update(cx, |finder, _| {
1634        assert_eq!(finder.delegate.matches.len(), 3);
1635        assert_match_at_position(finder, 0, "3.txt");
1636        assert_match_selection(finder, 1, "2.txt");
1637        assert_match_at_position(finder, 2, "1.txt");
1638    });
1639}
1640
1641#[gpui::test]
1642async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1643    let app_state = init_test(cx);
1644
1645    app_state
1646        .fs
1647        .as_fake()
1648        .insert_tree(
1649            path!("/src"),
1650            json!({
1651                "collab_ui": {
1652                    "first.rs": "// First Rust file",
1653                    "second.rs": "// Second Rust file",
1654                    "third.rs": "// Third Rust file",
1655                    "collab_ui.rs": "// Fourth Rust file",
1656                }
1657            }),
1658        )
1659        .await;
1660
1661    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1662    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1663    // generate some history to select from
1664    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1665    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1666    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1667    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1668
1669    let finder = open_file_picker(&workspace, cx);
1670    let query = "collab_ui";
1671    cx.simulate_input(query);
1672    finder.update(cx, |picker, _| {
1673            let search_entries = collect_search_matches(picker).search_paths_only();
1674            assert_eq!(
1675                search_entries,
1676                vec![
1677                    PathBuf::from("collab_ui/collab_ui.rs"),
1678                    PathBuf::from("collab_ui/first.rs"),
1679                    PathBuf::from("collab_ui/third.rs"),
1680                    PathBuf::from("collab_ui/second.rs"),
1681                ],
1682                "Despite all search results having the same directory name, the most matching one should be on top"
1683            );
1684        });
1685}
1686
1687#[gpui::test]
1688async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1689    let app_state = init_test(cx);
1690
1691    app_state
1692        .fs
1693        .as_fake()
1694        .insert_tree(
1695            path!("/src"),
1696            json!({
1697                "test": {
1698                    "first.rs": "// First Rust file",
1699                    "nonexistent.rs": "// Second Rust file",
1700                    "third.rs": "// Third Rust file",
1701                }
1702            }),
1703        )
1704        .await;
1705
1706    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1707    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
1708    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1709    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1710    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1711    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1712    app_state
1713        .fs
1714        .remove_file(
1715            Path::new(path!("/src/test/nonexistent.rs")),
1716            RemoveOptions::default(),
1717        )
1718        .await
1719        .unwrap();
1720    cx.run_until_parked();
1721
1722    let picker = open_file_picker(&workspace, cx);
1723    cx.simulate_input("rs");
1724
1725    picker.update(cx, |picker, _| {
1726        assert_eq!(
1727            collect_search_matches(picker).history,
1728            vec![
1729                PathBuf::from("test/first.rs"),
1730                PathBuf::from("test/third.rs"),
1731            ],
1732            "Should have all opened files in the history, except the ones that do not exist on disk"
1733        );
1734    });
1735}
1736
1737#[gpui::test]
1738async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1739    let app_state = init_test(cx);
1740
1741    app_state
1742        .fs
1743        .as_fake()
1744        .insert_tree(
1745            "/src",
1746            json!({
1747                "lib.rs": "// Lib file",
1748                "main.rs": "// Bar file",
1749                "read.me": "// Readme file",
1750            }),
1751        )
1752        .await;
1753
1754    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1755    let (workspace, cx) =
1756        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1757
1758    // Initial state
1759    let picker = open_file_picker(&workspace, cx);
1760    cx.simulate_input("rs");
1761    picker.update(cx, |finder, _| {
1762        assert_eq!(finder.delegate.matches.len(), 2);
1763        assert_match_at_position(finder, 0, "lib.rs");
1764        assert_match_at_position(finder, 1, "main.rs");
1765    });
1766
1767    // Delete main.rs
1768    app_state
1769        .fs
1770        .remove_file("/src/main.rs".as_ref(), Default::default())
1771        .await
1772        .expect("unable to remove file");
1773    cx.executor().advance_clock(FS_WATCH_LATENCY);
1774
1775    // main.rs is in not among search results anymore
1776    picker.update(cx, |finder, _| {
1777        assert_eq!(finder.delegate.matches.len(), 1);
1778        assert_match_at_position(finder, 0, "lib.rs");
1779    });
1780
1781    // Create util.rs
1782    app_state
1783        .fs
1784        .create_file("/src/util.rs".as_ref(), Default::default())
1785        .await
1786        .expect("unable to create file");
1787    cx.executor().advance_clock(FS_WATCH_LATENCY);
1788
1789    // util.rs is among search results
1790    picker.update(cx, |finder, _| {
1791        assert_eq!(finder.delegate.matches.len(), 2);
1792        assert_match_at_position(finder, 0, "lib.rs");
1793        assert_match_at_position(finder, 1, "util.rs");
1794    });
1795}
1796
1797#[gpui::test]
1798async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1799    cx: &mut gpui::TestAppContext,
1800) {
1801    let app_state = init_test(cx);
1802
1803    app_state
1804        .fs
1805        .as_fake()
1806        .insert_tree(
1807            "/test",
1808            json!({
1809                "project_1": {
1810                    "bar.rs": "// Bar file",
1811                    "lib.rs": "// Lib file",
1812                },
1813                "project_2": {
1814                    "Cargo.toml": "// Cargo file",
1815                    "main.rs": "// Main file",
1816                }
1817            }),
1818        )
1819        .await;
1820
1821    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1822    let (workspace, cx) =
1823        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1824    let worktree_1_id = project.update(cx, |project, cx| {
1825        let worktree = project.worktrees(cx).last().expect("worktree not found");
1826        worktree.read(cx).id()
1827    });
1828
1829    // Initial state
1830    let picker = open_file_picker(&workspace, cx);
1831    cx.simulate_input("rs");
1832    picker.update(cx, |finder, _| {
1833        assert_eq!(finder.delegate.matches.len(), 2);
1834        assert_match_at_position(finder, 0, "bar.rs");
1835        assert_match_at_position(finder, 1, "lib.rs");
1836    });
1837
1838    // Add new worktree
1839    project
1840        .update(cx, |project, cx| {
1841            project
1842                .find_or_create_worktree("/test/project_2", true, cx)
1843                .into_future()
1844        })
1845        .await
1846        .expect("unable to create workdir");
1847    cx.executor().advance_clock(FS_WATCH_LATENCY);
1848
1849    // main.rs is among search results
1850    picker.update(cx, |finder, _| {
1851        assert_eq!(finder.delegate.matches.len(), 3);
1852        assert_match_at_position(finder, 0, "bar.rs");
1853        assert_match_at_position(finder, 1, "lib.rs");
1854        assert_match_at_position(finder, 2, "main.rs");
1855    });
1856
1857    // Remove the first worktree
1858    project.update(cx, |project, cx| {
1859        project.remove_worktree(worktree_1_id, cx);
1860    });
1861    cx.executor().advance_clock(FS_WATCH_LATENCY);
1862
1863    // Files from the first worktree are not in the search results anymore
1864    picker.update(cx, |finder, _| {
1865        assert_eq!(finder.delegate.matches.len(), 1);
1866        assert_match_at_position(finder, 0, "main.rs");
1867    });
1868}
1869
1870#[gpui::test]
1871async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
1872    let app_state = init_test(cx);
1873
1874    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
1875
1876    app_state
1877        .fs
1878        .create_dir("/src/even".as_ref())
1879        .await
1880        .expect("unable to create dir");
1881
1882    let initial_files_num = 5;
1883    for i in 0..initial_files_num {
1884        let filename = format!("/src/even/file_{}.txt", 10 + i);
1885        app_state
1886            .fs
1887            .create_file(Path::new(&filename), Default::default())
1888            .await
1889            .expect("unable to create file");
1890    }
1891
1892    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1893    let (workspace, cx) =
1894        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1895
1896    // Initial state
1897    let picker = open_file_picker(&workspace, cx);
1898    cx.simulate_input("file");
1899    let selected_index = 3;
1900    // Checking only the filename, not the whole path
1901    let selected_file = format!("file_{}.txt", 10 + selected_index);
1902    // Select even/file_13.txt
1903    for _ in 0..selected_index {
1904        cx.dispatch_action(SelectNext);
1905    }
1906
1907    picker.update(cx, |finder, _| {
1908        assert_match_selection(finder, selected_index, &selected_file)
1909    });
1910
1911    // Add more matches to the search results
1912    let files_to_add = 10;
1913    for i in 0..files_to_add {
1914        let filename = format!("/src/file_{}.txt", 20 + i);
1915        app_state
1916            .fs
1917            .create_file(Path::new(&filename), Default::default())
1918            .await
1919            .expect("unable to create file");
1920    }
1921    cx.executor().advance_clock(FS_WATCH_LATENCY);
1922
1923    // file_13.txt is still selected
1924    picker.update(cx, |finder, _| {
1925        let expected_selected_index = selected_index + files_to_add;
1926        assert_match_selection(finder, expected_selected_index, &selected_file);
1927    });
1928}
1929
1930#[gpui::test]
1931async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
1932    cx: &mut gpui::TestAppContext,
1933) {
1934    let app_state = init_test(cx);
1935
1936    app_state
1937        .fs
1938        .as_fake()
1939        .insert_tree(
1940            "/src",
1941            json!({
1942                "file_1.txt": "// file_1",
1943                "file_2.txt": "// file_2",
1944                "file_3.txt": "// file_3",
1945            }),
1946        )
1947        .await;
1948
1949    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1950    let (workspace, cx) =
1951        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1952
1953    // Initial state
1954    let picker = open_file_picker(&workspace, cx);
1955    cx.simulate_input("file");
1956    // Select even/file_2.txt
1957    cx.dispatch_action(SelectNext);
1958
1959    // Remove the selected entry
1960    app_state
1961        .fs
1962        .remove_file("/src/file_2.txt".as_ref(), Default::default())
1963        .await
1964        .expect("unable to remove file");
1965    cx.executor().advance_clock(FS_WATCH_LATENCY);
1966
1967    // file_1.txt is now selected
1968    picker.update(cx, |finder, _| {
1969        assert_match_selection(finder, 0, "file_1.txt");
1970    });
1971}
1972
1973#[gpui::test]
1974async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1975    let app_state = init_test(cx);
1976
1977    app_state
1978        .fs
1979        .as_fake()
1980        .insert_tree(
1981            path!("/test"),
1982            json!({
1983                "1.txt": "// One",
1984            }),
1985        )
1986        .await;
1987
1988    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1989    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1990
1991    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1992
1993    cx.simulate_modifiers_change(Modifiers::secondary_key());
1994    open_file_picker(&workspace, cx);
1995
1996    cx.simulate_modifiers_change(Modifiers::none());
1997    active_file_picker(&workspace, cx);
1998}
1999
2000#[gpui::test]
2001async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2002    let app_state = init_test(cx);
2003
2004    app_state
2005        .fs
2006        .as_fake()
2007        .insert_tree(
2008            path!("/test"),
2009            json!({
2010                "1.txt": "// One",
2011                "2.txt": "// Two",
2012            }),
2013        )
2014        .await;
2015
2016    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2017    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2018
2019    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2020    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2021
2022    cx.simulate_modifiers_change(Modifiers::secondary_key());
2023    let picker = open_file_picker(&workspace, cx);
2024    picker.update(cx, |finder, _| {
2025        assert_eq!(finder.delegate.matches.len(), 2);
2026        assert_match_selection(finder, 0, "2.txt");
2027        assert_match_at_position(finder, 1, "1.txt");
2028    });
2029
2030    cx.dispatch_action(SelectNext);
2031    cx.simulate_modifiers_change(Modifiers::none());
2032    cx.read(|cx| {
2033        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2034        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2035    });
2036}
2037
2038#[gpui::test]
2039async fn test_switches_between_release_norelease_modes_on_forward_nav(
2040    cx: &mut gpui::TestAppContext,
2041) {
2042    let app_state = init_test(cx);
2043
2044    app_state
2045        .fs
2046        .as_fake()
2047        .insert_tree(
2048            path!("/test"),
2049            json!({
2050                "1.txt": "// One",
2051                "2.txt": "// Two",
2052            }),
2053        )
2054        .await;
2055
2056    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2057    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2058
2059    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2060    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2061
2062    // Open with a shortcut
2063    cx.simulate_modifiers_change(Modifiers::secondary_key());
2064    let picker = open_file_picker(&workspace, cx);
2065    picker.update(cx, |finder, _| {
2066        assert_eq!(finder.delegate.matches.len(), 2);
2067        assert_match_selection(finder, 0, "2.txt");
2068        assert_match_at_position(finder, 1, "1.txt");
2069    });
2070
2071    // Switch to navigating with other shortcuts
2072    // Don't open file on modifiers release
2073    cx.simulate_modifiers_change(Modifiers::control());
2074    cx.dispatch_action(SelectNext);
2075    cx.simulate_modifiers_change(Modifiers::none());
2076    picker.update(cx, |finder, _| {
2077        assert_eq!(finder.delegate.matches.len(), 2);
2078        assert_match_at_position(finder, 0, "2.txt");
2079        assert_match_selection(finder, 1, "1.txt");
2080    });
2081
2082    // Back to navigation with initial shortcut
2083    // Open file on modifiers release
2084    cx.simulate_modifiers_change(Modifiers::secondary_key());
2085    cx.dispatch_action(ToggleFileFinder::default());
2086    cx.simulate_modifiers_change(Modifiers::none());
2087    cx.read(|cx| {
2088        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2089        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
2090    });
2091}
2092
2093#[gpui::test]
2094async fn test_switches_between_release_norelease_modes_on_backward_nav(
2095    cx: &mut gpui::TestAppContext,
2096) {
2097    let app_state = init_test(cx);
2098
2099    app_state
2100        .fs
2101        .as_fake()
2102        .insert_tree(
2103            path!("/test"),
2104            json!({
2105                "1.txt": "// One",
2106                "2.txt": "// Two",
2107                "3.txt": "// Three"
2108            }),
2109        )
2110        .await;
2111
2112    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2113    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2114
2115    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2116    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2117    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2118
2119    // Open with a shortcut
2120    cx.simulate_modifiers_change(Modifiers::secondary_key());
2121    let picker = open_file_picker(&workspace, cx);
2122    picker.update(cx, |finder, _| {
2123        assert_eq!(finder.delegate.matches.len(), 3);
2124        assert_match_selection(finder, 0, "3.txt");
2125        assert_match_at_position(finder, 1, "2.txt");
2126        assert_match_at_position(finder, 2, "1.txt");
2127    });
2128
2129    // Switch to navigating with other shortcuts
2130    // Don't open file on modifiers release
2131    cx.simulate_modifiers_change(Modifiers::control());
2132    cx.dispatch_action(menu::SelectPrevious);
2133    cx.simulate_modifiers_change(Modifiers::none());
2134    picker.update(cx, |finder, _| {
2135        assert_eq!(finder.delegate.matches.len(), 3);
2136        assert_match_at_position(finder, 0, "3.txt");
2137        assert_match_at_position(finder, 1, "2.txt");
2138        assert_match_selection(finder, 2, "1.txt");
2139    });
2140
2141    // Back to navigation with initial shortcut
2142    // Open file on modifiers release
2143    cx.simulate_modifiers_change(Modifiers::secondary_key());
2144    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
2145    cx.simulate_modifiers_change(Modifiers::none());
2146    cx.read(|cx| {
2147        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2148        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
2149    });
2150}
2151
2152#[gpui::test]
2153async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
2154    let app_state = init_test(cx);
2155
2156    app_state
2157        .fs
2158        .as_fake()
2159        .insert_tree(
2160            path!("/test"),
2161            json!({
2162                "1.txt": "// One",
2163            }),
2164        )
2165        .await;
2166
2167    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2168    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2169
2170    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2171
2172    cx.simulate_modifiers_change(Modifiers::secondary_key());
2173    open_file_picker(&workspace, cx);
2174
2175    cx.simulate_modifiers_change(Modifiers::command_shift());
2176    active_file_picker(&workspace, cx);
2177}
2178
2179#[gpui::test]
2180async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
2181    let app_state = init_test(cx);
2182    app_state
2183        .fs
2184        .as_fake()
2185        .insert_tree(
2186            "/test",
2187            json!({
2188                "00.txt": "",
2189                "01.txt": "",
2190                "02.txt": "",
2191                "03.txt": "",
2192                "04.txt": "",
2193                "05.txt": "",
2194            }),
2195        )
2196        .await;
2197
2198    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
2199    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2200
2201    cx.dispatch_action(ToggleFileFinder::default());
2202    let picker = active_file_picker(&workspace, cx);
2203
2204    picker.update_in(cx, |picker, window, cx| {
2205        picker.update_matches(".txt".to_string(), window, cx)
2206    });
2207
2208    cx.run_until_parked();
2209
2210    picker.update(cx, |picker, _| {
2211        assert_eq!(picker.delegate.matches.len(), 6);
2212        assert_eq!(picker.delegate.selected_index, 0);
2213    });
2214
2215    // When toggling repeatedly, the picker scrolls to reveal the selected item.
2216    cx.dispatch_action(ToggleFileFinder::default());
2217    cx.dispatch_action(ToggleFileFinder::default());
2218    cx.dispatch_action(ToggleFileFinder::default());
2219
2220    cx.run_until_parked();
2221
2222    picker.update(cx, |picker, _| {
2223        assert_eq!(picker.delegate.matches.len(), 6);
2224        assert_eq!(picker.delegate.selected_index, 3);
2225    });
2226}
2227
2228async fn open_close_queried_buffer(
2229    input: &str,
2230    expected_matches: usize,
2231    expected_editor_title: &str,
2232    workspace: &Entity<Workspace>,
2233    cx: &mut gpui::VisualTestContext,
2234) -> Vec<FoundPath> {
2235    let history_items = open_queried_buffer(
2236        input,
2237        expected_matches,
2238        expected_editor_title,
2239        workspace,
2240        cx,
2241    )
2242    .await;
2243
2244    cx.dispatch_action(workspace::CloseActiveItem {
2245        save_intent: None,
2246        close_pinned: false,
2247    });
2248
2249    history_items
2250}
2251
2252async fn open_queried_buffer(
2253    input: &str,
2254    expected_matches: usize,
2255    expected_editor_title: &str,
2256    workspace: &Entity<Workspace>,
2257    cx: &mut gpui::VisualTestContext,
2258) -> Vec<FoundPath> {
2259    let picker = open_file_picker(&workspace, cx);
2260    cx.simulate_input(input);
2261
2262    let history_items = picker.update(cx, |finder, _| {
2263        assert_eq!(
2264            finder.delegate.matches.len(),
2265            expected_matches,
2266            "Unexpected number of matches found for query `{input}`, matches: {:?}",
2267            finder.delegate.matches
2268        );
2269        finder.delegate.history_items.clone()
2270    });
2271
2272    cx.dispatch_action(Confirm);
2273
2274    cx.read(|cx| {
2275        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2276        let active_editor_title = active_editor.read(cx).title(cx);
2277        assert_eq!(
2278            expected_editor_title, active_editor_title,
2279            "Unexpected editor title for query `{input}`"
2280        );
2281    });
2282
2283    history_items
2284}
2285
2286fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2287    cx.update(|cx| {
2288        let state = AppState::test(cx);
2289        theme::init(theme::LoadThemes::JustBase, cx);
2290        language::init(cx);
2291        super::init(cx);
2292        editor::init(cx);
2293        workspace::init_settings(cx);
2294        Project::init_settings(cx);
2295        state
2296    })
2297}
2298
2299fn test_path_position(test_str: &str) -> FileSearchQuery {
2300    let path_position = PathWithPosition::parse_str(test_str);
2301
2302    FileSearchQuery {
2303        raw_query: test_str.to_owned(),
2304        file_query_end: if path_position.path.to_str().unwrap() == test_str {
2305            None
2306        } else {
2307            Some(path_position.path.to_str().unwrap().len())
2308        },
2309        path_position,
2310    }
2311}
2312
2313fn build_find_picker(
2314    project: Entity<Project>,
2315    cx: &mut TestAppContext,
2316) -> (
2317    Entity<Picker<FileFinderDelegate>>,
2318    Entity<Workspace>,
2319    &mut VisualTestContext,
2320) {
2321    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2322    let picker = open_file_picker(&workspace, cx);
2323    (picker, workspace, cx)
2324}
2325
2326#[track_caller]
2327fn open_file_picker(
2328    workspace: &Entity<Workspace>,
2329    cx: &mut VisualTestContext,
2330) -> Entity<Picker<FileFinderDelegate>> {
2331    cx.dispatch_action(ToggleFileFinder {
2332        separate_history: true,
2333    });
2334    active_file_picker(workspace, cx)
2335}
2336
2337#[track_caller]
2338fn active_file_picker(
2339    workspace: &Entity<Workspace>,
2340    cx: &mut VisualTestContext,
2341) -> Entity<Picker<FileFinderDelegate>> {
2342    workspace.update(cx, |workspace, cx| {
2343        workspace
2344            .active_modal::<FileFinder>(cx)
2345            .expect("file finder is not open")
2346            .read(cx)
2347            .picker
2348            .clone()
2349    })
2350}
2351
2352#[derive(Debug, Default)]
2353struct SearchEntries {
2354    history: Vec<PathBuf>,
2355    history_found_paths: Vec<FoundPath>,
2356    search: Vec<PathBuf>,
2357    search_matches: Vec<PathMatch>,
2358}
2359
2360impl SearchEntries {
2361    #[track_caller]
2362    fn search_paths_only(self) -> Vec<PathBuf> {
2363        assert!(
2364            self.history.is_empty(),
2365            "Should have no history matches, but got: {:?}",
2366            self.history
2367        );
2368        self.search
2369    }
2370
2371    #[track_caller]
2372    fn search_matches_only(self) -> Vec<PathMatch> {
2373        assert!(
2374            self.history.is_empty(),
2375            "Should have no history matches, but got: {:?}",
2376            self.history
2377        );
2378        self.search_matches
2379    }
2380}
2381
2382fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2383    let mut search_entries = SearchEntries::default();
2384    for m in &picker.delegate.matches.matches {
2385        match &m {
2386            Match::History {
2387                path: history_path,
2388                panel_match: path_match,
2389            } => {
2390                search_entries.history.push(
2391                    path_match
2392                        .as_ref()
2393                        .map(|path_match| {
2394                            Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
2395                        })
2396                        .unwrap_or_else(|| {
2397                            history_path
2398                                .absolute
2399                                .as_deref()
2400                                .unwrap_or_else(|| &history_path.project.path)
2401                                .to_path_buf()
2402                        }),
2403                );
2404                search_entries
2405                    .history_found_paths
2406                    .push(history_path.clone());
2407            }
2408            Match::Search(path_match) => {
2409                search_entries
2410                    .search
2411                    .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
2412                search_entries.search_matches.push(path_match.0.clone());
2413            }
2414        }
2415    }
2416    search_entries
2417}
2418
2419#[track_caller]
2420fn assert_match_selection(
2421    finder: &Picker<FileFinderDelegate>,
2422    expected_selection_index: usize,
2423    expected_file_name: &str,
2424) {
2425    assert_eq!(
2426        finder.delegate.selected_index(),
2427        expected_selection_index,
2428        "Match is not selected"
2429    );
2430    assert_match_at_position(finder, expected_selection_index, expected_file_name);
2431}
2432
2433#[track_caller]
2434fn assert_match_at_position(
2435    finder: &Picker<FileFinderDelegate>,
2436    match_index: usize,
2437    expected_file_name: &str,
2438) {
2439    let match_item = finder
2440        .delegate
2441        .matches
2442        .get(match_index)
2443        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2444    let match_file_name = match &match_item {
2445        Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
2446        Match::Search(path_match) => path_match.0.path.file_name(),
2447    }
2448    .unwrap()
2449    .to_string_lossy();
2450    assert_eq!(match_file_name, expected_file_name);
2451}
2452
2453#[gpui::test]
2454async fn test_filename_precedence(cx: &mut TestAppContext) {
2455    let app_state = init_test(cx);
2456
2457    app_state
2458        .fs
2459        .as_fake()
2460        .insert_tree(
2461            path!("/src"),
2462            json!({
2463                "layout": {
2464                    "app.css": "",
2465                    "app.d.ts": "",
2466                    "app.html": "",
2467                    "+page.svelte": "",
2468                },
2469                "routes": {
2470                    "+layout.svelte": "",
2471                }
2472            }),
2473        )
2474        .await;
2475
2476    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2477    let (picker, _, cx) = build_find_picker(project, cx);
2478
2479    cx.simulate_input("layout");
2480
2481    picker.update(cx, |finder, _| {
2482        let search_matches = collect_search_matches(finder).search_paths_only();
2483
2484        assert_eq!(
2485            search_matches,
2486            vec![
2487                PathBuf::from("routes/+layout.svelte"),
2488                PathBuf::from("layout/app.css"),
2489                PathBuf::from("layout/app.d.ts"),
2490                PathBuf::from("layout/app.html"),
2491                PathBuf::from("layout/+page.svelte"),
2492            ],
2493            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
2494        );
2495    });
2496}