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