file_finder_tests.rs

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