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