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