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