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, assert_matches};
   8use project::{FS_WATCH_LATENCY, RemoveOptions};
   9use serde_json::json;
  10use util::{path, rel_path::rel_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: rel_path("b0.5").into(),
  81            path_prefix: rel_path("").into(),
  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: rel_path("c1.0").into(),
  90            path_prefix: rel_path("").into(),
  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: rel_path("a1.0").into(),
  99            path_prefix: rel_path("").into(),
 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: rel_path("a0.5").into(),
 108            path_prefix: rel_path("").into(),
 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: rel_path("b1.0").into(),
 117            path_prefix: rel_path("").into(),
 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: rel_path("a1.0").into(),
 132                path_prefix: rel_path("").into(),
 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: rel_path("b1.0").into(),
 141                path_prefix: rel_path("").into(),
 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: rel_path("c1.0").into(),
 150                path_prefix: rel_path("").into(),
 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: rel_path("a0.5").into(),
 159                path_prefix: rel_path("").into(),
 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: rel_path("b0.5").into(),
 168                path_prefix: rel_path("").into(),
 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![rel_path("a/b/file2.txt").into()],
 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::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![rel_path("其他/S数据表格/task.xlsx").into()],
 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                rel_path("ignored-root/hi").into(),
 717                rel_path("tracked-root/hi").into(),
 718                rel_path("ignored-root/hiccup").into(),
 719                rel_path("tracked-root/hiccup").into(),
 720                rel_path("ignored-root/height").into(),
 721                rel_path("ignored-root/happiness").into(),
 722                rel_path("tracked-root/happiness").into(),
 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                rel_path("ignored-root/hi").into(),
 742                rel_path("tracked-root/hi").into(),
 743                rel_path("ignored-root/hiccup").into(),
 744                rel_path("tracked-root/hiccup").into(),
 745                rel_path("ignored-root/height").into(),
 746                rel_path("tracked-root/height").into(),
 747                rel_path("ignored-root/happiness").into(),
 748                rel_path("tracked-root/happiness").into(),
 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                rel_path("tracked-root/hi").into(),
 769                rel_path("tracked-root/hiccup").into(),
 770                rel_path("tracked-root/happiness").into(),
 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                rel_path("ignored-root/hi").into(),
 816                rel_path("tracked-root/hi").into(),
 817                rel_path("ignored-root/hiccup").into(),
 818                rel_path("tracked-root/hiccup").into(),
 819                rel_path("ignored-root/height").into(),
 820                rel_path("ignored-root/happiness").into(),
 821                rel_path("tracked-root/happiness").into(),
 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                rel_path("ignored-root/hi").into(),
 842                rel_path("tracked-root/hi").into(),
 843                rel_path("ignored-root/hiccup").into(),
 844                rel_path("tracked-root/hiccup").into(),
 845                rel_path("ignored-root/height").into(),
 846                rel_path("tracked-root/height").into(),
 847                rel_path("tracked-root/heights/height_1").into(),
 848                rel_path("tracked-root/heights/height_2").into(),
 849                rel_path("ignored-root/happiness").into(),
 850                rel_path("tracked-root/happiness").into(),
 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                rel_path("tracked-root/hi").into(),
 871                rel_path("tracked-root/hiccup").into(),
 872                rel_path("tracked-root/happiness").into(),
 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], PathStyle::local());
 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: rel_path("the-parent-dirb/fileb").into(),
 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: rel_path("the-parent-dirb/filec").into()
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        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1042    });
1043
1044    let finder = open_file_picker(&workspace, cx);
1045
1046    finder
1047        .update_in(cx, |f, window, cx| {
1048            f.delegate
1049                .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1050        })
1051        .await;
1052    cx.run_until_parked();
1053    finder.update_in(cx, |picker, window, cx| {
1054        assert_eq!(picker.delegate.matches.len(), 1);
1055        picker.delegate.confirm(false, window, cx)
1056    });
1057    cx.run_until_parked();
1058    cx.read(|cx| {
1059        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1060        let project_path = active_editor.read(cx).project_path(cx);
1061        assert_eq!(
1062            project_path,
1063            Some(ProjectPath {
1064                worktree_id: worktree_id2,
1065                path: rel_path("filec").into()
1066            })
1067        );
1068    });
1069}
1070
1071#[gpui::test]
1072async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1073    let app_state = init_test(cx);
1074    app_state
1075        .fs
1076        .as_fake()
1077        .insert_tree(
1078            path!("/root"),
1079            json!({
1080                "dir1": { "a.txt": "" },
1081                "dir2": {
1082                    "a.txt": "",
1083                    "b.txt": ""
1084                }
1085            }),
1086        )
1087        .await;
1088
1089    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1090    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1091
1092    let worktree_id = cx.read(|cx| {
1093        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1094        assert_eq!(worktrees.len(), 1);
1095        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1096    });
1097
1098    // When workspace has an active item, sort items which are closer to that item
1099    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1100    // so that one should be sorted earlier
1101    let b_path = ProjectPath {
1102        worktree_id,
1103        path: rel_path("dir2/b.txt").into(),
1104    };
1105    workspace
1106        .update_in(cx, |workspace, window, cx| {
1107            workspace.open_path(b_path, None, true, window, cx)
1108        })
1109        .await
1110        .unwrap();
1111    let finder = open_file_picker(&workspace, cx);
1112    finder
1113        .update_in(cx, |f, window, cx| {
1114            f.delegate
1115                .spawn_search(test_path_position("a.txt"), window, cx)
1116        })
1117        .await;
1118
1119    finder.update(cx, |picker, _| {
1120        let matches = collect_search_matches(picker).search_paths_only();
1121        assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1122        assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1123    });
1124}
1125
1126#[gpui::test]
1127async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1128    let app_state = init_test(cx);
1129    app_state
1130        .fs
1131        .as_fake()
1132        .insert_tree(
1133            "/root",
1134            json!({
1135                "dir1": {},
1136                "dir2": {
1137                    "dir3": {}
1138                }
1139            }),
1140        )
1141        .await;
1142
1143    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1144    let (picker, _workspace, cx) = build_find_picker(project, cx);
1145
1146    picker
1147        .update_in(cx, |f, window, cx| {
1148            f.delegate
1149                .spawn_search(test_path_position("dir"), window, cx)
1150        })
1151        .await;
1152    cx.read(|cx| {
1153        let finder = picker.read(cx);
1154        assert_eq!(finder.delegate.matches.len(), 1);
1155        assert_match_at_position(finder, 0, "dir");
1156    });
1157}
1158
1159#[gpui::test]
1160async fn test_query_history(cx: &mut gpui::TestAppContext) {
1161    let app_state = init_test(cx);
1162
1163    app_state
1164        .fs
1165        .as_fake()
1166        .insert_tree(
1167            path!("/src"),
1168            json!({
1169                "test": {
1170                    "first.rs": "// First Rust file",
1171                    "second.rs": "// Second Rust file",
1172                    "third.rs": "// Third Rust file",
1173                }
1174            }),
1175        )
1176        .await;
1177
1178    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1179    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1180    let worktree_id = cx.read(|cx| {
1181        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1182        assert_eq!(worktrees.len(), 1);
1183        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1184    });
1185
1186    // Open and close panels, getting their history items afterwards.
1187    // Ensure history items get populated with opened items, and items are kept in a certain order.
1188    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1189    //
1190    // TODO: without closing, the opened items do not propagate their history changes for some reason
1191    // it does work in real app though, only tests do not propagate.
1192    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1193
1194    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1195    assert!(
1196        initial_history.is_empty(),
1197        "Should have no history before opening any files"
1198    );
1199
1200    let history_after_first =
1201        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1202    assert_eq!(
1203        history_after_first,
1204        vec![FoundPath::new(
1205            ProjectPath {
1206                worktree_id,
1207                path: rel_path("test/first.rs").into(),
1208            },
1209            PathBuf::from(path!("/src/test/first.rs"))
1210        )],
1211        "Should show 1st opened item in the history when opening the 2nd item"
1212    );
1213
1214    let history_after_second =
1215        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1216    assert_eq!(
1217        history_after_second,
1218        vec![
1219            FoundPath::new(
1220                ProjectPath {
1221                    worktree_id,
1222                    path: rel_path("test/second.rs").into(),
1223                },
1224                PathBuf::from(path!("/src/test/second.rs"))
1225            ),
1226            FoundPath::new(
1227                ProjectPath {
1228                    worktree_id,
1229                    path: rel_path("test/first.rs").into(),
1230                },
1231                PathBuf::from(path!("/src/test/first.rs"))
1232            ),
1233        ],
1234        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1235    2nd item should be the first in the history, as the last opened."
1236    );
1237
1238    let history_after_third =
1239        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1240    assert_eq!(
1241        history_after_third,
1242        vec![
1243            FoundPath::new(
1244                ProjectPath {
1245                    worktree_id,
1246                    path: rel_path("test/third.rs").into(),
1247                },
1248                PathBuf::from(path!("/src/test/third.rs"))
1249            ),
1250            FoundPath::new(
1251                ProjectPath {
1252                    worktree_id,
1253                    path: rel_path("test/second.rs").into(),
1254                },
1255                PathBuf::from(path!("/src/test/second.rs"))
1256            ),
1257            FoundPath::new(
1258                ProjectPath {
1259                    worktree_id,
1260                    path: rel_path("test/first.rs").into(),
1261                },
1262                PathBuf::from(path!("/src/test/first.rs"))
1263            ),
1264        ],
1265        "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1266    3rd item should be the first in the history, as the last opened."
1267    );
1268
1269    let history_after_second_again =
1270        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1271    assert_eq!(
1272        history_after_second_again,
1273        vec![
1274            FoundPath::new(
1275                ProjectPath {
1276                    worktree_id,
1277                    path: rel_path("test/second.rs").into(),
1278                },
1279                PathBuf::from(path!("/src/test/second.rs"))
1280            ),
1281            FoundPath::new(
1282                ProjectPath {
1283                    worktree_id,
1284                    path: rel_path("test/third.rs").into(),
1285                },
1286                PathBuf::from(path!("/src/test/third.rs"))
1287            ),
1288            FoundPath::new(
1289                ProjectPath {
1290                    worktree_id,
1291                    path: rel_path("test/first.rs").into(),
1292                },
1293                PathBuf::from(path!("/src/test/first.rs"))
1294            ),
1295        ],
1296        "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1297    2nd item, as the last opened, 3rd item should go next as it was opened right before."
1298    );
1299}
1300
1301#[gpui::test]
1302async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1303    let app_state = init_test(cx);
1304
1305    app_state
1306        .fs
1307        .as_fake()
1308        .insert_tree(
1309            path!("/src"),
1310            json!({
1311                "test": {
1312                    "first.rs": "// First Rust file",
1313                    "second.rs": "// Second Rust file",
1314                    "third.rs": "// Third Rust file",
1315                }
1316            }),
1317        )
1318        .await;
1319
1320    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1321    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1322
1323    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1324
1325    open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
1326    let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
1327    assert_eq!(history.len(), 1);
1328
1329    let picker = open_file_picker(&workspace, cx);
1330    cx.simulate_input("fir");
1331    picker.update_in(cx, |finder, window, cx| {
1332        let matches = &finder.delegate.matches.matches;
1333        assert_matches!(
1334            matches.as_slice(),
1335            [Match::History { .. }, Match::CreateNew { .. }]
1336        );
1337        assert_eq!(
1338            matches[0].panel_match().unwrap().0.path.as_ref(),
1339            rel_path("test/first.rs")
1340        );
1341        assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
1342
1343        let (file_label, path_label) =
1344            finder
1345                .delegate
1346                .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
1347        assert_eq!(file_label.text(), "first.rs");
1348        assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
1349        assert_eq!(
1350            path_label.text(),
1351            format!("test{}", PathStyle::local().separator())
1352        );
1353        assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
1354    });
1355}
1356
1357#[gpui::test]
1358async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1359    let app_state = init_test(cx);
1360
1361    app_state
1362        .fs
1363        .as_fake()
1364        .insert_tree(
1365            path!("/src"),
1366            json!({
1367                "test": {
1368                    "first.rs": "// First Rust file",
1369                    "second.rs": "// Second Rust file",
1370                }
1371            }),
1372        )
1373        .await;
1374
1375    app_state
1376        .fs
1377        .as_fake()
1378        .insert_tree(
1379            path!("/external-src"),
1380            json!({
1381                "test": {
1382                    "third.rs": "// Third Rust file",
1383                    "fourth.rs": "// Fourth Rust file",
1384                }
1385            }),
1386        )
1387        .await;
1388
1389    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1390    cx.update(|cx| {
1391        project.update(cx, |project, cx| {
1392            project.find_or_create_worktree(path!("/external-src"), false, cx)
1393        })
1394    })
1395    .detach();
1396    cx.background_executor.run_until_parked();
1397
1398    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1399    let worktree_id = cx.read(|cx| {
1400        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1401        assert_eq!(worktrees.len(), 1,);
1402
1403        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1404    });
1405    workspace
1406        .update_in(cx, |workspace, window, cx| {
1407            workspace.open_abs_path(
1408                PathBuf::from(path!("/external-src/test/third.rs")),
1409                OpenOptions {
1410                    visible: Some(OpenVisible::None),
1411                    ..Default::default()
1412                },
1413                window,
1414                cx,
1415            )
1416        })
1417        .detach();
1418    cx.background_executor.run_until_parked();
1419    let external_worktree_id = cx.read(|cx| {
1420        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1421        assert_eq!(
1422            worktrees.len(),
1423            2,
1424            "External file should get opened in a new worktree"
1425        );
1426
1427        WorktreeId::from_usize(
1428            worktrees
1429                .into_iter()
1430                .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
1431                .expect("New worktree should have a different id")
1432                .entity_id()
1433                .as_u64() as usize,
1434        )
1435    });
1436    cx.dispatch_action(workspace::CloseActiveItem {
1437        save_intent: None,
1438        close_pinned: false,
1439    });
1440
1441    let initial_history_items =
1442        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1443    assert_eq!(
1444        initial_history_items,
1445        vec![FoundPath::new(
1446            ProjectPath {
1447                worktree_id: external_worktree_id,
1448                path: rel_path("").into(),
1449            },
1450            PathBuf::from(path!("/external-src/test/third.rs"))
1451        )],
1452        "Should show external file with its full path in the history after it was open"
1453    );
1454
1455    let updated_history_items =
1456        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1457    assert_eq!(
1458        updated_history_items,
1459        vec![
1460            FoundPath::new(
1461                ProjectPath {
1462                    worktree_id,
1463                    path: rel_path("test/second.rs").into(),
1464                },
1465                PathBuf::from(path!("/src/test/second.rs"))
1466            ),
1467            FoundPath::new(
1468                ProjectPath {
1469                    worktree_id: external_worktree_id,
1470                    path: rel_path("").into(),
1471                },
1472                PathBuf::from(path!("/external-src/test/third.rs"))
1473            ),
1474        ],
1475        "Should keep external file with history updates",
1476    );
1477}
1478
1479#[gpui::test]
1480async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1481    let app_state = init_test(cx);
1482
1483    app_state
1484        .fs
1485        .as_fake()
1486        .insert_tree(
1487            path!("/src"),
1488            json!({
1489                "test": {
1490                    "first.rs": "// First Rust file",
1491                    "second.rs": "// Second Rust file",
1492                    "third.rs": "// Third Rust file",
1493                }
1494            }),
1495        )
1496        .await;
1497
1498    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1499    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1500
1501    // generate some history to select from
1502    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1503    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1504    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1505    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1506
1507    for expected_selected_index in 0..current_history.len() {
1508        cx.dispatch_action(ToggleFileFinder::default());
1509        let picker = active_file_picker(&workspace, cx);
1510        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1511        assert_eq!(
1512            selected_index, expected_selected_index,
1513            "Should select the next item in the history"
1514        );
1515    }
1516
1517    cx.dispatch_action(ToggleFileFinder::default());
1518    let selected_index = workspace.update(cx, |workspace, cx| {
1519        workspace
1520            .active_modal::<FileFinder>(cx)
1521            .unwrap()
1522            .read(cx)
1523            .picker
1524            .read(cx)
1525            .delegate
1526            .selected_index()
1527    });
1528    assert_eq!(
1529        selected_index, 0,
1530        "Should wrap around the history and start all over"
1531    );
1532}
1533
1534#[gpui::test]
1535async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1536    let app_state = init_test(cx);
1537
1538    app_state
1539        .fs
1540        .as_fake()
1541        .insert_tree(
1542            path!("/src"),
1543            json!({
1544                "test": {
1545                    "first.rs": "// First Rust file",
1546                    "second.rs": "// Second Rust file",
1547                    "third.rs": "// Third Rust file",
1548                    "fourth.rs": "// Fourth Rust file",
1549                }
1550            }),
1551        )
1552        .await;
1553
1554    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1555    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1556    let worktree_id = cx.read(|cx| {
1557        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1558        assert_eq!(worktrees.len(), 1,);
1559
1560        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1561    });
1562
1563    // generate some history to select from
1564    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1565    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1566    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1567    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1568
1569    let finder = open_file_picker(&workspace, cx);
1570    let first_query = "f";
1571    finder
1572        .update_in(cx, |finder, window, cx| {
1573            finder
1574                .delegate
1575                .update_matches(first_query.to_string(), window, cx)
1576        })
1577        .await;
1578    finder.update(cx, |picker, _| {
1579            let matches = collect_search_matches(picker);
1580            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1581            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1582            assert_eq!(history_match, &FoundPath::new(
1583                ProjectPath {
1584                    worktree_id,
1585                    path: rel_path("test/first.rs").into(),
1586                },
1587                PathBuf::from(path!("/src/test/first.rs")),
1588            ));
1589            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1590            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1591        });
1592
1593    let second_query = "fsdasdsa";
1594    let finder = active_file_picker(&workspace, cx);
1595    finder
1596        .update_in(cx, |finder, window, cx| {
1597            finder
1598                .delegate
1599                .update_matches(second_query.to_string(), window, cx)
1600        })
1601        .await;
1602    finder.update(cx, |picker, _| {
1603        assert!(
1604            collect_search_matches(picker)
1605                .search_paths_only()
1606                .is_empty(),
1607            "No search entries should match {second_query}"
1608        );
1609    });
1610
1611    let first_query_again = first_query;
1612
1613    let finder = active_file_picker(&workspace, cx);
1614    finder
1615        .update_in(cx, |finder, window, cx| {
1616            finder
1617                .delegate
1618                .update_matches(first_query_again.to_string(), window, cx)
1619        })
1620        .await;
1621    finder.update(cx, |picker, _| {
1622            let matches = collect_search_matches(picker);
1623            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");
1624            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1625            assert_eq!(history_match, &FoundPath::new(
1626                ProjectPath {
1627                    worktree_id,
1628                    path: rel_path("test/first.rs").into(),
1629                },
1630                PathBuf::from(path!("/src/test/first.rs"))
1631            ));
1632            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1633            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1634        });
1635}
1636
1637#[gpui::test]
1638async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1639    let app_state = init_test(cx);
1640
1641    app_state
1642        .fs
1643        .as_fake()
1644        .insert_tree(
1645            path!("/root"),
1646            json!({
1647                "test": {
1648                    "1_qw": "// First file that matches the query",
1649                    "2_second": "// Second file",
1650                    "3_third": "// Third file",
1651                    "4_fourth": "// Fourth file",
1652                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1653                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1654                    "7_qwqwqw": "// One more, same amount of query matches as above",
1655                }
1656            }),
1657        )
1658        .await;
1659
1660    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1661    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1662    // generate some history to select from
1663    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1664    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1665    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1666    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1667    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1668
1669    let finder = open_file_picker(&workspace, cx);
1670    let query = "qw";
1671    finder
1672        .update_in(cx, |finder, window, cx| {
1673            finder
1674                .delegate
1675                .update_matches(query.to_string(), window, cx)
1676        })
1677        .await;
1678    finder.update(cx, |finder, _| {
1679        let search_matches = collect_search_matches(finder);
1680        assert_eq!(
1681            search_matches.history,
1682            vec![
1683                rel_path("test/1_qw").into(),
1684                rel_path("test/6_qwqwqw").into()
1685            ],
1686        );
1687        assert_eq!(
1688            search_matches.search,
1689            vec![
1690                rel_path("test/5_qwqwqw").into(),
1691                rel_path("test/7_qwqwqw").into()
1692            ],
1693        );
1694    });
1695}
1696
1697#[gpui::test]
1698async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1699    let app_state = init_test(cx);
1700
1701    app_state
1702        .fs
1703        .as_fake()
1704        .insert_tree(
1705            path!("/root"),
1706            json!({
1707                "test": {
1708                    "1_qw": "",
1709                }
1710            }),
1711        )
1712        .await;
1713
1714    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1715    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1716    // Open new buffer
1717    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1718
1719    let picker = open_file_picker(&workspace, cx);
1720    picker.update(cx, |finder, _| {
1721        assert_match_selection(finder, 0, "1_qw");
1722    });
1723}
1724
1725#[gpui::test]
1726async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1727    cx: &mut TestAppContext,
1728) {
1729    let app_state = init_test(cx);
1730
1731    app_state
1732        .fs
1733        .as_fake()
1734        .insert_tree(
1735            path!("/src"),
1736            json!({
1737                "test": {
1738                    "bar.rs": "// Bar file",
1739                    "lib.rs": "// Lib file",
1740                    "maaa.rs": "// Maaaaaaa",
1741                    "main.rs": "// Main file",
1742                    "moo.rs": "// Moooooo",
1743                }
1744            }),
1745        )
1746        .await;
1747
1748    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1749    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1750
1751    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1752    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1753    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1754
1755    // main.rs is on top, previously used is selected
1756    let picker = open_file_picker(&workspace, cx);
1757    picker.update(cx, |finder, _| {
1758        assert_eq!(finder.delegate.matches.len(), 3);
1759        assert_match_selection(finder, 0, "main.rs");
1760        assert_match_at_position(finder, 1, "lib.rs");
1761        assert_match_at_position(finder, 2, "bar.rs");
1762    });
1763
1764    // all files match, main.rs is still on top, but the second item is selected
1765    picker
1766        .update_in(cx, |finder, window, cx| {
1767            finder
1768                .delegate
1769                .update_matches(".rs".to_string(), window, cx)
1770        })
1771        .await;
1772    picker.update(cx, |finder, _| {
1773        assert_eq!(finder.delegate.matches.len(), 6);
1774        assert_match_at_position(finder, 0, "main.rs");
1775        assert_match_selection(finder, 1, "bar.rs");
1776        assert_match_at_position(finder, 2, "lib.rs");
1777        assert_match_at_position(finder, 3, "moo.rs");
1778        assert_match_at_position(finder, 4, "maaa.rs");
1779        assert_match_at_position(finder, 5, ".rs");
1780    });
1781
1782    // main.rs is not among matches, select top item
1783    picker
1784        .update_in(cx, |finder, window, cx| {
1785            finder.delegate.update_matches("b".to_string(), window, cx)
1786        })
1787        .await;
1788    picker.update(cx, |finder, _| {
1789        assert_eq!(finder.delegate.matches.len(), 3);
1790        assert_match_at_position(finder, 0, "bar.rs");
1791        assert_match_at_position(finder, 1, "lib.rs");
1792        assert_match_at_position(finder, 2, "b");
1793    });
1794
1795    // main.rs is back, put it on top and select next item
1796    picker
1797        .update_in(cx, |finder, window, cx| {
1798            finder.delegate.update_matches("m".to_string(), window, cx)
1799        })
1800        .await;
1801    picker.update(cx, |finder, _| {
1802        assert_eq!(finder.delegate.matches.len(), 4);
1803        assert_match_at_position(finder, 0, "main.rs");
1804        assert_match_selection(finder, 1, "moo.rs");
1805        assert_match_at_position(finder, 2, "maaa.rs");
1806        assert_match_at_position(finder, 3, "m");
1807    });
1808
1809    // get back to the initial state
1810    picker
1811        .update_in(cx, |finder, window, cx| {
1812            finder.delegate.update_matches("".to_string(), window, cx)
1813        })
1814        .await;
1815    picker.update(cx, |finder, _| {
1816        assert_eq!(finder.delegate.matches.len(), 3);
1817        assert_match_selection(finder, 0, "main.rs");
1818        assert_match_at_position(finder, 1, "lib.rs");
1819        assert_match_at_position(finder, 2, "bar.rs");
1820    });
1821}
1822
1823#[gpui::test]
1824async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
1825    let app_state = init_test(cx);
1826
1827    cx.update(|cx| {
1828        let settings = *FileFinderSettings::get_global(cx);
1829
1830        FileFinderSettings::override_global(
1831            FileFinderSettings {
1832                skip_focus_for_active_in_search: false,
1833                ..settings
1834            },
1835            cx,
1836        );
1837    });
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    // main.rs is on top, previously used is selected
1864    let picker = open_file_picker(&workspace, cx);
1865    picker.update(cx, |finder, _| {
1866        assert_eq!(finder.delegate.matches.len(), 3);
1867        assert_match_selection(finder, 0, "main.rs");
1868        assert_match_at_position(finder, 1, "lib.rs");
1869        assert_match_at_position(finder, 2, "bar.rs");
1870    });
1871
1872    // all files match, main.rs is on top, and is selected
1873    picker
1874        .update_in(cx, |finder, window, cx| {
1875            finder
1876                .delegate
1877                .update_matches(".rs".to_string(), window, cx)
1878        })
1879        .await;
1880    picker.update(cx, |finder, _| {
1881        assert_eq!(finder.delegate.matches.len(), 6);
1882        assert_match_selection(finder, 0, "main.rs");
1883        assert_match_at_position(finder, 1, "bar.rs");
1884        assert_match_at_position(finder, 2, "lib.rs");
1885        assert_match_at_position(finder, 3, "moo.rs");
1886        assert_match_at_position(finder, 4, "maaa.rs");
1887        assert_match_at_position(finder, 5, ".rs");
1888    });
1889}
1890
1891#[gpui::test]
1892async fn test_non_separate_history_items(cx: &mut TestAppContext) {
1893    let app_state = init_test(cx);
1894
1895    app_state
1896        .fs
1897        .as_fake()
1898        .insert_tree(
1899            path!("/src"),
1900            json!({
1901                "test": {
1902                    "bar.rs": "// Bar file",
1903                    "lib.rs": "// Lib file",
1904                    "maaa.rs": "// Maaaaaaa",
1905                    "main.rs": "// Main file",
1906                    "moo.rs": "// Moooooo",
1907                }
1908            }),
1909        )
1910        .await;
1911
1912    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1913    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1914
1915    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1916    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1917    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1918
1919    cx.dispatch_action(ToggleFileFinder::default());
1920    let picker = active_file_picker(&workspace, cx);
1921    // main.rs is on top, previously used is selected
1922    picker.update(cx, |finder, _| {
1923        assert_eq!(finder.delegate.matches.len(), 3);
1924        assert_match_selection(finder, 0, "main.rs");
1925        assert_match_at_position(finder, 1, "lib.rs");
1926        assert_match_at_position(finder, 2, "bar.rs");
1927    });
1928
1929    // all files match, main.rs is still on top, but the second item is selected
1930    picker
1931        .update_in(cx, |finder, window, cx| {
1932            finder
1933                .delegate
1934                .update_matches(".rs".to_string(), window, cx)
1935        })
1936        .await;
1937    picker.update(cx, |finder, _| {
1938        assert_eq!(finder.delegate.matches.len(), 6);
1939        assert_match_at_position(finder, 0, "main.rs");
1940        assert_match_selection(finder, 1, "moo.rs");
1941        assert_match_at_position(finder, 2, "bar.rs");
1942        assert_match_at_position(finder, 3, "lib.rs");
1943        assert_match_at_position(finder, 4, "maaa.rs");
1944        assert_match_at_position(finder, 5, ".rs");
1945    });
1946
1947    // main.rs is not among matches, select top item
1948    picker
1949        .update_in(cx, |finder, window, cx| {
1950            finder.delegate.update_matches("b".to_string(), window, cx)
1951        })
1952        .await;
1953    picker.update(cx, |finder, _| {
1954        assert_eq!(finder.delegate.matches.len(), 3);
1955        assert_match_at_position(finder, 0, "bar.rs");
1956        assert_match_at_position(finder, 1, "lib.rs");
1957        assert_match_at_position(finder, 2, "b");
1958    });
1959
1960    // main.rs is back, put it on top and select next item
1961    picker
1962        .update_in(cx, |finder, window, cx| {
1963            finder.delegate.update_matches("m".to_string(), window, cx)
1964        })
1965        .await;
1966    picker.update(cx, |finder, _| {
1967        assert_eq!(finder.delegate.matches.len(), 4);
1968        assert_match_at_position(finder, 0, "main.rs");
1969        assert_match_selection(finder, 1, "moo.rs");
1970        assert_match_at_position(finder, 2, "maaa.rs");
1971        assert_match_at_position(finder, 3, "m");
1972    });
1973
1974    // get back to the initial state
1975    picker
1976        .update_in(cx, |finder, window, cx| {
1977            finder.delegate.update_matches("".to_string(), window, cx)
1978        })
1979        .await;
1980    picker.update(cx, |finder, _| {
1981        assert_eq!(finder.delegate.matches.len(), 3);
1982        assert_match_selection(finder, 0, "main.rs");
1983        assert_match_at_position(finder, 1, "lib.rs");
1984        assert_match_at_position(finder, 2, "bar.rs");
1985    });
1986}
1987
1988#[gpui::test]
1989async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1990    let app_state = init_test(cx);
1991
1992    app_state
1993        .fs
1994        .as_fake()
1995        .insert_tree(
1996            path!("/test"),
1997            json!({
1998                "test": {
1999                    "1.txt": "// One",
2000                    "2.txt": "// Two",
2001                    "3.txt": "// Three",
2002                }
2003            }),
2004        )
2005        .await;
2006
2007    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2008    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2009
2010    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2011    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2012    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2013
2014    let picker = open_file_picker(&workspace, cx);
2015    picker.update(cx, |finder, _| {
2016        assert_eq!(finder.delegate.matches.len(), 3);
2017        assert_match_selection(finder, 0, "3.txt");
2018        assert_match_at_position(finder, 1, "2.txt");
2019        assert_match_at_position(finder, 2, "1.txt");
2020    });
2021
2022    cx.dispatch_action(SelectNext);
2023    cx.dispatch_action(Confirm); // Open 2.txt
2024
2025    let picker = open_file_picker(&workspace, cx);
2026    picker.update(cx, |finder, _| {
2027        assert_eq!(finder.delegate.matches.len(), 3);
2028        assert_match_selection(finder, 0, "2.txt");
2029        assert_match_at_position(finder, 1, "3.txt");
2030        assert_match_at_position(finder, 2, "1.txt");
2031    });
2032
2033    cx.dispatch_action(SelectNext);
2034    cx.dispatch_action(SelectNext);
2035    cx.dispatch_action(Confirm); // Open 1.txt
2036
2037    let picker = open_file_picker(&workspace, cx);
2038    picker.update(cx, |finder, _| {
2039        assert_eq!(finder.delegate.matches.len(), 3);
2040        assert_match_selection(finder, 0, "1.txt");
2041        assert_match_at_position(finder, 1, "2.txt");
2042        assert_match_at_position(finder, 2, "3.txt");
2043    });
2044}
2045
2046#[gpui::test]
2047async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2048    let app_state = init_test(cx);
2049
2050    app_state
2051        .fs
2052        .as_fake()
2053        .insert_tree(
2054            path!("/test"),
2055            json!({
2056                "test": {
2057                    "1.txt": "// One",
2058                    "2.txt": "// Two",
2059                    "3.txt": "// Three",
2060                }
2061            }),
2062        )
2063        .await;
2064
2065    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2066    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2067
2068    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2069    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2070    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2071
2072    let picker = open_file_picker(&workspace, cx);
2073    picker.update(cx, |finder, _| {
2074        assert_eq!(finder.delegate.matches.len(), 3);
2075        assert_match_selection(finder, 0, "3.txt");
2076        assert_match_at_position(finder, 1, "2.txt");
2077        assert_match_at_position(finder, 2, "1.txt");
2078    });
2079
2080    cx.dispatch_action(SelectNext);
2081
2082    // Add more files to the worktree to trigger update matches
2083    for i in 0..5 {
2084        let filename = if cfg!(windows) {
2085            format!("C:/test/{}.txt", 4 + i)
2086        } else {
2087            format!("/test/{}.txt", 4 + i)
2088        };
2089        app_state
2090            .fs
2091            .create_file(Path::new(&filename), Default::default())
2092            .await
2093            .expect("unable to create file");
2094    }
2095
2096    cx.executor().advance_clock(FS_WATCH_LATENCY);
2097
2098    picker.update(cx, |finder, _| {
2099        assert_eq!(finder.delegate.matches.len(), 3);
2100        assert_match_at_position(finder, 0, "3.txt");
2101        assert_match_selection(finder, 1, "2.txt");
2102        assert_match_at_position(finder, 2, "1.txt");
2103    });
2104}
2105
2106#[gpui::test]
2107async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2108    let app_state = init_test(cx);
2109
2110    app_state
2111        .fs
2112        .as_fake()
2113        .insert_tree(
2114            path!("/src"),
2115            json!({
2116                "collab_ui": {
2117                    "first.rs": "// First Rust file",
2118                    "second.rs": "// Second Rust file",
2119                    "third.rs": "// Third Rust file",
2120                    "collab_ui.rs": "// Fourth Rust file",
2121                }
2122            }),
2123        )
2124        .await;
2125
2126    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2127    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2128    // generate some history to select from
2129    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2130    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2131    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2132    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2133
2134    let finder = open_file_picker(&workspace, cx);
2135    let query = "collab_ui";
2136    cx.simulate_input(query);
2137    finder.update(cx, |picker, _| {
2138            let search_entries = collect_search_matches(picker).search_paths_only();
2139            assert_eq!(
2140                search_entries,
2141                vec![
2142                    rel_path("collab_ui/collab_ui.rs").into(),
2143                    rel_path("collab_ui/first.rs").into(),
2144                    rel_path("collab_ui/third.rs").into(),
2145                    rel_path("collab_ui/second.rs").into(),
2146                ],
2147                "Despite all search results having the same directory name, the most matching one should be on top"
2148            );
2149        });
2150}
2151
2152#[gpui::test]
2153async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2154    let app_state = init_test(cx);
2155
2156    app_state
2157        .fs
2158        .as_fake()
2159        .insert_tree(
2160            path!("/src"),
2161            json!({
2162                "test": {
2163                    "first.rs": "// First Rust file",
2164                    "nonexistent.rs": "// Second Rust file",
2165                    "third.rs": "// Third Rust file",
2166                }
2167            }),
2168        )
2169        .await;
2170
2171    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2172    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
2173    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2174    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2175    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2176    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2177    app_state
2178        .fs
2179        .remove_file(
2180            Path::new(path!("/src/test/nonexistent.rs")),
2181            RemoveOptions::default(),
2182        )
2183        .await
2184        .unwrap();
2185    cx.run_until_parked();
2186
2187    let picker = open_file_picker(&workspace, cx);
2188    cx.simulate_input("rs");
2189
2190    picker.update(cx, |picker, _| {
2191        assert_eq!(
2192            collect_search_matches(picker).history,
2193            vec![
2194                rel_path("test/first.rs").into(),
2195                rel_path("test/third.rs").into()
2196            ],
2197            "Should have all opened files in the history, except the ones that do not exist on disk"
2198        );
2199    });
2200}
2201
2202#[gpui::test]
2203async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2204    let app_state = init_test(cx);
2205
2206    app_state
2207        .fs
2208        .as_fake()
2209        .insert_tree(
2210            "/src",
2211            json!({
2212                "lib.rs": "// Lib file",
2213                "main.rs": "// Bar file",
2214                "read.me": "// Readme file",
2215            }),
2216        )
2217        .await;
2218
2219    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2220    let (workspace, cx) =
2221        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2222
2223    // Initial state
2224    let picker = open_file_picker(&workspace, cx);
2225    cx.simulate_input("rs");
2226    picker.update(cx, |finder, _| {
2227        assert_eq!(finder.delegate.matches.len(), 3);
2228        assert_match_at_position(finder, 0, "lib.rs");
2229        assert_match_at_position(finder, 1, "main.rs");
2230        assert_match_at_position(finder, 2, "rs");
2231    });
2232
2233    // Delete main.rs
2234    app_state
2235        .fs
2236        .remove_file("/src/main.rs".as_ref(), Default::default())
2237        .await
2238        .expect("unable to remove file");
2239    cx.executor().advance_clock(FS_WATCH_LATENCY);
2240
2241    // main.rs is in not among search results anymore
2242    picker.update(cx, |finder, _| {
2243        assert_eq!(finder.delegate.matches.len(), 2);
2244        assert_match_at_position(finder, 0, "lib.rs");
2245        assert_match_at_position(finder, 1, "rs");
2246    });
2247
2248    // Create util.rs
2249    app_state
2250        .fs
2251        .create_file("/src/util.rs".as_ref(), Default::default())
2252        .await
2253        .expect("unable to create file");
2254    cx.executor().advance_clock(FS_WATCH_LATENCY);
2255
2256    // util.rs is among search results
2257    picker.update(cx, |finder, _| {
2258        assert_eq!(finder.delegate.matches.len(), 3);
2259        assert_match_at_position(finder, 0, "lib.rs");
2260        assert_match_at_position(finder, 1, "util.rs");
2261        assert_match_at_position(finder, 2, "rs");
2262    });
2263}
2264
2265#[gpui::test]
2266async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2267    cx: &mut gpui::TestAppContext,
2268) {
2269    let app_state = init_test(cx);
2270
2271    app_state
2272        .fs
2273        .as_fake()
2274        .insert_tree(
2275            "/test",
2276            json!({
2277                "project_1": {
2278                    "bar.rs": "// Bar file",
2279                    "lib.rs": "// Lib file",
2280                },
2281                "project_2": {
2282                    "Cargo.toml": "// Cargo file",
2283                    "main.rs": "// Main file",
2284                }
2285            }),
2286        )
2287        .await;
2288
2289    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2290    let (workspace, cx) =
2291        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2292    let worktree_1_id = project.update(cx, |project, cx| {
2293        let worktree = project.worktrees(cx).last().expect("worktree not found");
2294        worktree.read(cx).id()
2295    });
2296
2297    // Initial state
2298    let picker = open_file_picker(&workspace, cx);
2299    cx.simulate_input("rs");
2300    picker.update(cx, |finder, _| {
2301        assert_eq!(finder.delegate.matches.len(), 3);
2302        assert_match_at_position(finder, 0, "bar.rs");
2303        assert_match_at_position(finder, 1, "lib.rs");
2304        assert_match_at_position(finder, 2, "rs");
2305    });
2306
2307    // Add new worktree
2308    project
2309        .update(cx, |project, cx| {
2310            project
2311                .find_or_create_worktree("/test/project_2", true, cx)
2312                .into_future()
2313        })
2314        .await
2315        .expect("unable to create workdir");
2316    cx.executor().advance_clock(FS_WATCH_LATENCY);
2317
2318    // main.rs is among search results
2319    picker.update(cx, |finder, _| {
2320        assert_eq!(finder.delegate.matches.len(), 4);
2321        assert_match_at_position(finder, 0, "bar.rs");
2322        assert_match_at_position(finder, 1, "lib.rs");
2323        assert_match_at_position(finder, 2, "main.rs");
2324        assert_match_at_position(finder, 3, "rs");
2325    });
2326
2327    // Remove the first worktree
2328    project.update(cx, |project, cx| {
2329        project.remove_worktree(worktree_1_id, cx);
2330    });
2331    cx.executor().advance_clock(FS_WATCH_LATENCY);
2332
2333    // Files from the first worktree are not in the search results anymore
2334    picker.update(cx, |finder, _| {
2335        assert_eq!(finder.delegate.matches.len(), 2);
2336        assert_match_at_position(finder, 0, "main.rs");
2337        assert_match_at_position(finder, 1, "rs");
2338    });
2339}
2340
2341#[gpui::test]
2342async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2343    let app_state = init_test(cx);
2344
2345    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2346
2347    app_state
2348        .fs
2349        .create_dir("/src/even".as_ref())
2350        .await
2351        .expect("unable to create dir");
2352
2353    let initial_files_num = 5;
2354    for i in 0..initial_files_num {
2355        let filename = format!("/src/even/file_{}.txt", 10 + i);
2356        app_state
2357            .fs
2358            .create_file(Path::new(&filename), Default::default())
2359            .await
2360            .expect("unable to create file");
2361    }
2362
2363    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2364    let (workspace, cx) =
2365        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2366
2367    // Initial state
2368    let picker = open_file_picker(&workspace, cx);
2369    cx.simulate_input("file");
2370    let selected_index = 3;
2371    // Checking only the filename, not the whole path
2372    let selected_file = format!("file_{}.txt", 10 + selected_index);
2373    // Select even/file_13.txt
2374    for _ in 0..selected_index {
2375        cx.dispatch_action(SelectNext);
2376    }
2377
2378    picker.update(cx, |finder, _| {
2379        assert_match_selection(finder, selected_index, &selected_file)
2380    });
2381
2382    // Add more matches to the search results
2383    let files_to_add = 10;
2384    for i in 0..files_to_add {
2385        let filename = format!("/src/file_{}.txt", 20 + i);
2386        app_state
2387            .fs
2388            .create_file(Path::new(&filename), Default::default())
2389            .await
2390            .expect("unable to create file");
2391    }
2392    cx.executor().advance_clock(FS_WATCH_LATENCY);
2393
2394    // file_13.txt is still selected
2395    picker.update(cx, |finder, _| {
2396        let expected_selected_index = selected_index + files_to_add;
2397        assert_match_selection(finder, expected_selected_index, &selected_file);
2398    });
2399}
2400
2401#[gpui::test]
2402async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2403    cx: &mut gpui::TestAppContext,
2404) {
2405    let app_state = init_test(cx);
2406
2407    app_state
2408        .fs
2409        .as_fake()
2410        .insert_tree(
2411            "/src",
2412            json!({
2413                "file_1.txt": "// file_1",
2414                "file_2.txt": "// file_2",
2415                "file_3.txt": "// file_3",
2416            }),
2417        )
2418        .await;
2419
2420    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2421    let (workspace, cx) =
2422        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2423
2424    // Initial state
2425    let picker = open_file_picker(&workspace, cx);
2426    cx.simulate_input("file");
2427    // Select even/file_2.txt
2428    cx.dispatch_action(SelectNext);
2429
2430    // Remove the selected entry
2431    app_state
2432        .fs
2433        .remove_file("/src/file_2.txt".as_ref(), Default::default())
2434        .await
2435        .expect("unable to remove file");
2436    cx.executor().advance_clock(FS_WATCH_LATENCY);
2437
2438    // file_1.txt is now selected
2439    picker.update(cx, |finder, _| {
2440        assert_match_selection(finder, 0, "file_1.txt");
2441    });
2442}
2443
2444#[gpui::test]
2445async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2446    let app_state = init_test(cx);
2447
2448    app_state
2449        .fs
2450        .as_fake()
2451        .insert_tree(
2452            path!("/test"),
2453            json!({
2454                "1.txt": "// One",
2455            }),
2456        )
2457        .await;
2458
2459    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2460    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2461
2462    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2463
2464    cx.simulate_modifiers_change(Modifiers::secondary_key());
2465    open_file_picker(&workspace, cx);
2466
2467    cx.simulate_modifiers_change(Modifiers::none());
2468    active_file_picker(&workspace, cx);
2469}
2470
2471#[gpui::test]
2472async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2473    let app_state = init_test(cx);
2474
2475    app_state
2476        .fs
2477        .as_fake()
2478        .insert_tree(
2479            path!("/test"),
2480            json!({
2481                "1.txt": "// One",
2482                "2.txt": "// Two",
2483            }),
2484        )
2485        .await;
2486
2487    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2488    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2489
2490    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2491    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2492
2493    cx.simulate_modifiers_change(Modifiers::secondary_key());
2494    let picker = open_file_picker(&workspace, cx);
2495    picker.update(cx, |finder, _| {
2496        assert_eq!(finder.delegate.matches.len(), 2);
2497        assert_match_selection(finder, 0, "2.txt");
2498        assert_match_at_position(finder, 1, "1.txt");
2499    });
2500
2501    cx.dispatch_action(SelectNext);
2502    cx.simulate_modifiers_change(Modifiers::none());
2503    cx.read(|cx| {
2504        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2505        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2506    });
2507}
2508
2509#[gpui::test]
2510async fn test_switches_between_release_norelease_modes_on_forward_nav(
2511    cx: &mut gpui::TestAppContext,
2512) {
2513    let app_state = init_test(cx);
2514
2515    app_state
2516        .fs
2517        .as_fake()
2518        .insert_tree(
2519            path!("/test"),
2520            json!({
2521                "1.txt": "// One",
2522                "2.txt": "// Two",
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
2533    // Open with a shortcut
2534    cx.simulate_modifiers_change(Modifiers::secondary_key());
2535    let picker = open_file_picker(&workspace, cx);
2536    picker.update(cx, |finder, _| {
2537        assert_eq!(finder.delegate.matches.len(), 2);
2538        assert_match_selection(finder, 0, "2.txt");
2539        assert_match_at_position(finder, 1, "1.txt");
2540    });
2541
2542    // Switch to navigating with other shortcuts
2543    // Don't open file on modifiers release
2544    cx.simulate_modifiers_change(Modifiers::control());
2545    cx.dispatch_action(SelectNext);
2546    cx.simulate_modifiers_change(Modifiers::none());
2547    picker.update(cx, |finder, _| {
2548        assert_eq!(finder.delegate.matches.len(), 2);
2549        assert_match_at_position(finder, 0, "2.txt");
2550        assert_match_selection(finder, 1, "1.txt");
2551    });
2552
2553    // Back to navigation with initial shortcut
2554    // Open file on modifiers release
2555    cx.simulate_modifiers_change(Modifiers::secondary_key());
2556    cx.dispatch_action(ToggleFileFinder::default());
2557    cx.simulate_modifiers_change(Modifiers::none());
2558    cx.read(|cx| {
2559        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2560        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
2561    });
2562}
2563
2564#[gpui::test]
2565async fn test_switches_between_release_norelease_modes_on_backward_nav(
2566    cx: &mut gpui::TestAppContext,
2567) {
2568    let app_state = init_test(cx);
2569
2570    app_state
2571        .fs
2572        .as_fake()
2573        .insert_tree(
2574            path!("/test"),
2575            json!({
2576                "1.txt": "// One",
2577                "2.txt": "// Two",
2578                "3.txt": "// Three"
2579            }),
2580        )
2581        .await;
2582
2583    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2584    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2585
2586    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2587    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2588    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2589
2590    // Open with a shortcut
2591    cx.simulate_modifiers_change(Modifiers::secondary_key());
2592    let picker = open_file_picker(&workspace, cx);
2593    picker.update(cx, |finder, _| {
2594        assert_eq!(finder.delegate.matches.len(), 3);
2595        assert_match_selection(finder, 0, "3.txt");
2596        assert_match_at_position(finder, 1, "2.txt");
2597        assert_match_at_position(finder, 2, "1.txt");
2598    });
2599
2600    // Switch to navigating with other shortcuts
2601    // Don't open file on modifiers release
2602    cx.simulate_modifiers_change(Modifiers::control());
2603    cx.dispatch_action(menu::SelectPrevious);
2604    cx.simulate_modifiers_change(Modifiers::none());
2605    picker.update(cx, |finder, _| {
2606        assert_eq!(finder.delegate.matches.len(), 3);
2607        assert_match_at_position(finder, 0, "3.txt");
2608        assert_match_at_position(finder, 1, "2.txt");
2609        assert_match_selection(finder, 2, "1.txt");
2610    });
2611
2612    // Back to navigation with initial shortcut
2613    // Open file on modifiers release
2614    cx.simulate_modifiers_change(Modifiers::secondary_key());
2615    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
2616    cx.simulate_modifiers_change(Modifiers::none());
2617    cx.read(|cx| {
2618        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2619        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
2620    });
2621}
2622
2623#[gpui::test]
2624async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
2625    let app_state = init_test(cx);
2626
2627    app_state
2628        .fs
2629        .as_fake()
2630        .insert_tree(
2631            path!("/test"),
2632            json!({
2633                "1.txt": "// One",
2634            }),
2635        )
2636        .await;
2637
2638    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2639    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2640
2641    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2642
2643    cx.simulate_modifiers_change(Modifiers::secondary_key());
2644    open_file_picker(&workspace, cx);
2645
2646    cx.simulate_modifiers_change(Modifiers::command_shift());
2647    active_file_picker(&workspace, cx);
2648}
2649
2650#[gpui::test]
2651async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
2652    let app_state = init_test(cx);
2653    app_state
2654        .fs
2655        .as_fake()
2656        .insert_tree(
2657            "/test",
2658            json!({
2659                "00.txt": "",
2660                "01.txt": "",
2661                "02.txt": "",
2662                "03.txt": "",
2663                "04.txt": "",
2664                "05.txt": "",
2665            }),
2666        )
2667        .await;
2668
2669    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
2670    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2671
2672    cx.dispatch_action(ToggleFileFinder::default());
2673    let picker = active_file_picker(&workspace, cx);
2674
2675    picker.update_in(cx, |picker, window, cx| {
2676        picker.update_matches(".txt".to_string(), window, cx)
2677    });
2678
2679    cx.run_until_parked();
2680
2681    picker.update(cx, |picker, _| {
2682        assert_eq!(picker.delegate.matches.len(), 7);
2683        assert_eq!(picker.delegate.selected_index, 0);
2684    });
2685
2686    // When toggling repeatedly, the picker scrolls to reveal the selected item.
2687    cx.dispatch_action(ToggleFileFinder::default());
2688    cx.dispatch_action(ToggleFileFinder::default());
2689    cx.dispatch_action(ToggleFileFinder::default());
2690
2691    cx.run_until_parked();
2692
2693    picker.update(cx, |picker, _| {
2694        assert_eq!(picker.delegate.matches.len(), 7);
2695        assert_eq!(picker.delegate.selected_index, 3);
2696    });
2697}
2698
2699async fn open_close_queried_buffer(
2700    input: &str,
2701    expected_matches: usize,
2702    expected_editor_title: &str,
2703    workspace: &Entity<Workspace>,
2704    cx: &mut gpui::VisualTestContext,
2705) -> Vec<FoundPath> {
2706    let history_items = open_queried_buffer(
2707        input,
2708        expected_matches,
2709        expected_editor_title,
2710        workspace,
2711        cx,
2712    )
2713    .await;
2714
2715    cx.dispatch_action(workspace::CloseActiveItem {
2716        save_intent: None,
2717        close_pinned: false,
2718    });
2719
2720    history_items
2721}
2722
2723async fn open_queried_buffer(
2724    input: &str,
2725    expected_matches: usize,
2726    expected_editor_title: &str,
2727    workspace: &Entity<Workspace>,
2728    cx: &mut gpui::VisualTestContext,
2729) -> Vec<FoundPath> {
2730    let picker = open_file_picker(workspace, cx);
2731    cx.simulate_input(input);
2732
2733    let history_items = picker.update(cx, |finder, _| {
2734        assert_eq!(
2735            finder.delegate.matches.len(),
2736            expected_matches + 1, // +1 from CreateNew option
2737            "Unexpected number of matches found for query `{input}`, matches: {:?}",
2738            finder.delegate.matches
2739        );
2740        finder.delegate.history_items.clone()
2741    });
2742
2743    cx.dispatch_action(Confirm);
2744
2745    cx.read(|cx| {
2746        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2747        let active_editor_title = active_editor.read(cx).title(cx);
2748        assert_eq!(
2749            expected_editor_title, active_editor_title,
2750            "Unexpected editor title for query `{input}`"
2751        );
2752    });
2753
2754    history_items
2755}
2756
2757fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2758    cx.update(|cx| {
2759        let state = AppState::test(cx);
2760        theme::init(theme::LoadThemes::JustBase, cx);
2761        language::init(cx);
2762        super::init(cx);
2763        editor::init(cx);
2764        workspace::init_settings(cx);
2765        Project::init_settings(cx);
2766        state
2767    })
2768}
2769
2770fn test_path_position(test_str: &str) -> FileSearchQuery {
2771    let path_position = PathWithPosition::parse_str(test_str);
2772
2773    FileSearchQuery {
2774        raw_query: test_str.to_owned(),
2775        file_query_end: if path_position.path.to_str().unwrap() == test_str {
2776            None
2777        } else {
2778            Some(path_position.path.to_str().unwrap().len())
2779        },
2780        path_position,
2781    }
2782}
2783
2784fn build_find_picker(
2785    project: Entity<Project>,
2786    cx: &mut TestAppContext,
2787) -> (
2788    Entity<Picker<FileFinderDelegate>>,
2789    Entity<Workspace>,
2790    &mut VisualTestContext,
2791) {
2792    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2793    let picker = open_file_picker(&workspace, cx);
2794    (picker, workspace, cx)
2795}
2796
2797#[track_caller]
2798fn open_file_picker(
2799    workspace: &Entity<Workspace>,
2800    cx: &mut VisualTestContext,
2801) -> Entity<Picker<FileFinderDelegate>> {
2802    cx.dispatch_action(ToggleFileFinder {
2803        separate_history: true,
2804    });
2805    active_file_picker(workspace, cx)
2806}
2807
2808#[track_caller]
2809fn active_file_picker(
2810    workspace: &Entity<Workspace>,
2811    cx: &mut VisualTestContext,
2812) -> Entity<Picker<FileFinderDelegate>> {
2813    workspace.update(cx, |workspace, cx| {
2814        workspace
2815            .active_modal::<FileFinder>(cx)
2816            .expect("file finder is not open")
2817            .read(cx)
2818            .picker
2819            .clone()
2820    })
2821}
2822
2823#[derive(Debug, Default)]
2824struct SearchEntries {
2825    history: Vec<Arc<RelPath>>,
2826    history_found_paths: Vec<FoundPath>,
2827    search: Vec<Arc<RelPath>>,
2828    search_matches: Vec<PathMatch>,
2829}
2830
2831impl SearchEntries {
2832    #[track_caller]
2833    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
2834        assert!(
2835            self.history.is_empty(),
2836            "Should have no history matches, but got: {:?}",
2837            self.history
2838        );
2839        self.search
2840    }
2841
2842    #[track_caller]
2843    fn search_matches_only(self) -> Vec<PathMatch> {
2844        assert!(
2845            self.history.is_empty(),
2846            "Should have no history matches, but got: {:?}",
2847            self.history
2848        );
2849        self.search_matches
2850    }
2851}
2852
2853fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2854    let mut search_entries = SearchEntries::default();
2855    for m in &picker.delegate.matches.matches {
2856        match &m {
2857            Match::History {
2858                path: history_path,
2859                panel_match: path_match,
2860            } => {
2861                if let Some(path_match) = path_match.as_ref() {
2862                    search_entries
2863                        .history
2864                        .push(path_match.0.path_prefix.join(&path_match.0.path));
2865                } else {
2866                    // This occurs when the query is empty and we show history matches
2867                    // that are outside the project.
2868                    panic!("currently not exercised in tests");
2869                }
2870                search_entries
2871                    .history_found_paths
2872                    .push(history_path.clone());
2873            }
2874            Match::Search(path_match) => {
2875                search_entries
2876                    .search
2877                    .push(path_match.0.path_prefix.join(&path_match.0.path));
2878                search_entries.search_matches.push(path_match.0.clone());
2879            }
2880            Match::CreateNew(_) => {}
2881        }
2882    }
2883    search_entries
2884}
2885
2886#[track_caller]
2887fn assert_match_selection(
2888    finder: &Picker<FileFinderDelegate>,
2889    expected_selection_index: usize,
2890    expected_file_name: &str,
2891) {
2892    assert_eq!(
2893        finder.delegate.selected_index(),
2894        expected_selection_index,
2895        "Match is not selected"
2896    );
2897    assert_match_at_position(finder, expected_selection_index, expected_file_name);
2898}
2899
2900#[track_caller]
2901fn assert_match_at_position(
2902    finder: &Picker<FileFinderDelegate>,
2903    match_index: usize,
2904    expected_file_name: &str,
2905) {
2906    let match_item = finder
2907        .delegate
2908        .matches
2909        .get(match_index)
2910        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2911    let match_file_name = match &match_item {
2912        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
2913        Match::Search(path_match) => path_match.0.path.file_name(),
2914        Match::CreateNew(project_path) => project_path.path.file_name(),
2915    }
2916    .unwrap();
2917    assert_eq!(match_file_name, expected_file_name);
2918}
2919
2920#[gpui::test]
2921async fn test_filename_precedence(cx: &mut TestAppContext) {
2922    let app_state = init_test(cx);
2923
2924    app_state
2925        .fs
2926        .as_fake()
2927        .insert_tree(
2928            path!("/src"),
2929            json!({
2930                "layout": {
2931                    "app.css": "",
2932                    "app.d.ts": "",
2933                    "app.html": "",
2934                    "+page.svelte": "",
2935                },
2936                "routes": {
2937                    "+layout.svelte": "",
2938                }
2939            }),
2940        )
2941        .await;
2942
2943    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2944    let (picker, _, cx) = build_find_picker(project, cx);
2945
2946    cx.simulate_input("layout");
2947
2948    picker.update(cx, |finder, _| {
2949        let search_matches = collect_search_matches(finder).search_paths_only();
2950
2951        assert_eq!(
2952            search_matches,
2953            vec![
2954                rel_path("routes/+layout.svelte").into(),
2955                rel_path("layout/app.css").into(),
2956                rel_path("layout/app.d.ts").into(),
2957                rel_path("layout/app.html").into(),
2958                rel_path("layout/+page.svelte").into(),
2959            ],
2960            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
2961        );
2962    });
2963}