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