file_finder_tests.rs

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