file_finder_tests.rs

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