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