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