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, assert_matches};
   8use project::{FS_WATCH_LATENCY, RemoveOptions};
   9use serde_json::json;
  10use settings::SettingsStore;
  11use util::{path, rel_path::rel_path};
  12use workspace::{
  13    AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths,
  14};
  15
  16#[ctor::ctor]
  17fn init_logger() {
  18    zlog::init_test();
  19}
  20
  21#[test]
  22fn test_path_elision() {
  23    #[track_caller]
  24    fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
  25        let mut path = path.to_owned();
  26        let slice = PathComponentSlice::new(&path);
  27        let matches = Vec::from_iter(matches);
  28        if let Some(range) = slice.elision_range(budget - 1, &matches) {
  29            path.replace_range(range, "");
  30        }
  31        assert_eq!(path, expected);
  32    }
  33
  34    // Simple cases, mostly to check that different path shapes are handled gracefully.
  35    check("p/a/b/c/d/", 6, [], "p/…/d/");
  36    check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
  37    check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
  38    check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
  39
  40    check("p/a/b/c/d", 5, [], "p/…/d");
  41    check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
  42    check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
  43    check("p/a/b/c/d", 7, [6], "p/…/c/d");
  44
  45    check("/p/a/b/c/d/", 7, [], "/p/…/d/");
  46    check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
  47    check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
  48    check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
  49
  50    // If the budget can't be met, no elision is done.
  51    check(
  52        "project/dir/child/grandchild",
  53        5,
  54        [],
  55        "project/dir/child/grandchild",
  56    );
  57
  58    // The longest unmatched segment is picked for elision.
  59    check(
  60        "project/one/two/X/three/sub",
  61        21,
  62        [16],
  63        "project/…/X/three/sub",
  64    );
  65
  66    // Elision stops when the budget is met, even though there are more components in the chosen segment.
  67    // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
  68    check(
  69        "project/one/two/three/X/sub",
  70        21,
  71        [22],
  72        "project/…/three/X/sub",
  73    )
  74}
  75
  76#[test]
  77fn test_custom_project_search_ordering_in_file_finder() {
  78    let mut file_finder_sorted_output = vec![
  79        ProjectPanelOrdMatch(PathMatch {
  80            score: 0.5,
  81            positions: Vec::new(),
  82            worktree_id: 0,
  83            path: rel_path("b0.5").into(),
  84            path_prefix: rel_path("").into(),
  85            distance_to_relative_ancestor: 0,
  86            is_dir: false,
  87        }),
  88        ProjectPanelOrdMatch(PathMatch {
  89            score: 1.0,
  90            positions: Vec::new(),
  91            worktree_id: 0,
  92            path: rel_path("c1.0").into(),
  93            path_prefix: rel_path("").into(),
  94            distance_to_relative_ancestor: 0,
  95            is_dir: false,
  96        }),
  97        ProjectPanelOrdMatch(PathMatch {
  98            score: 1.0,
  99            positions: Vec::new(),
 100            worktree_id: 0,
 101            path: rel_path("a1.0").into(),
 102            path_prefix: rel_path("").into(),
 103            distance_to_relative_ancestor: 0,
 104            is_dir: false,
 105        }),
 106        ProjectPanelOrdMatch(PathMatch {
 107            score: 0.5,
 108            positions: Vec::new(),
 109            worktree_id: 0,
 110            path: rel_path("a0.5").into(),
 111            path_prefix: rel_path("").into(),
 112            distance_to_relative_ancestor: 0,
 113            is_dir: false,
 114        }),
 115        ProjectPanelOrdMatch(PathMatch {
 116            score: 1.0,
 117            positions: Vec::new(),
 118            worktree_id: 0,
 119            path: rel_path("b1.0").into(),
 120            path_prefix: rel_path("").into(),
 121            distance_to_relative_ancestor: 0,
 122            is_dir: false,
 123        }),
 124    ];
 125    file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
 126
 127    assert_eq!(
 128        file_finder_sorted_output,
 129        vec![
 130            ProjectPanelOrdMatch(PathMatch {
 131                score: 1.0,
 132                positions: Vec::new(),
 133                worktree_id: 0,
 134                path: rel_path("a1.0").into(),
 135                path_prefix: rel_path("").into(),
 136                distance_to_relative_ancestor: 0,
 137                is_dir: false,
 138            }),
 139            ProjectPanelOrdMatch(PathMatch {
 140                score: 1.0,
 141                positions: Vec::new(),
 142                worktree_id: 0,
 143                path: rel_path("b1.0").into(),
 144                path_prefix: rel_path("").into(),
 145                distance_to_relative_ancestor: 0,
 146                is_dir: false,
 147            }),
 148            ProjectPanelOrdMatch(PathMatch {
 149                score: 1.0,
 150                positions: Vec::new(),
 151                worktree_id: 0,
 152                path: rel_path("c1.0").into(),
 153                path_prefix: rel_path("").into(),
 154                distance_to_relative_ancestor: 0,
 155                is_dir: false,
 156            }),
 157            ProjectPanelOrdMatch(PathMatch {
 158                score: 0.5,
 159                positions: Vec::new(),
 160                worktree_id: 0,
 161                path: rel_path("a0.5").into(),
 162                path_prefix: rel_path("").into(),
 163                distance_to_relative_ancestor: 0,
 164                is_dir: false,
 165            }),
 166            ProjectPanelOrdMatch(PathMatch {
 167                score: 0.5,
 168                positions: Vec::new(),
 169                worktree_id: 0,
 170                path: rel_path("b0.5").into(),
 171                path_prefix: rel_path("").into(),
 172                distance_to_relative_ancestor: 0,
 173                is_dir: false,
 174            }),
 175        ]
 176    );
 177}
 178
 179#[gpui::test]
 180async fn test_matching_paths(cx: &mut TestAppContext) {
 181    let app_state = init_test(cx);
 182    app_state
 183        .fs
 184        .as_fake()
 185        .insert_tree(
 186            path!("/root"),
 187            json!({
 188                "a": {
 189                    "banana": "",
 190                    "bandana": "",
 191                }
 192            }),
 193        )
 194        .await;
 195
 196    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 197
 198    let (picker, workspace, cx) = build_find_picker(project, cx);
 199
 200    cx.simulate_input("bna");
 201    picker.update(cx, |picker, _| {
 202        assert_eq!(picker.delegate.matches.len(), 3);
 203    });
 204    cx.dispatch_action(SelectNext);
 205    cx.dispatch_action(Confirm);
 206    cx.read(|cx| {
 207        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 208        assert_eq!(active_editor.read(cx).title(cx), "bandana");
 209    });
 210
 211    for bandana_query in [
 212        "bandana",
 213        "./bandana",
 214        ".\\bandana",
 215        util::path!("a/bandana"),
 216        "b/bandana",
 217        "b\\bandana",
 218        " bandana",
 219        "bandana ",
 220        " bandana ",
 221        " ndan ",
 222        " band ",
 223        "a bandana",
 224        "bandana:",
 225    ] {
 226        picker
 227            .update_in(cx, |picker, window, cx| {
 228                picker
 229                    .delegate
 230                    .update_matches(bandana_query.to_string(), window, cx)
 231            })
 232            .await;
 233        picker.update(cx, |picker, _| {
 234            assert_eq!(
 235                picker.delegate.matches.len(),
 236                // existence of CreateNew option depends on whether path already exists
 237                if bandana_query == util::path!("a/bandana") {
 238                    1
 239                } else {
 240                    2
 241                },
 242                "Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
 243                picker.delegate.matches
 244            );
 245        });
 246        cx.dispatch_action(SelectNext);
 247        cx.dispatch_action(Confirm);
 248        cx.read(|cx| {
 249            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 250            assert_eq!(
 251                active_editor.read(cx).title(cx),
 252                "bandana",
 253                "Wrong match for bandana query '{bandana_query}'"
 254            );
 255        });
 256    }
 257}
 258
 259#[gpui::test]
 260async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
 261    let app_state = init_test(cx);
 262    app_state
 263        .fs
 264        .as_fake()
 265        .insert_tree(
 266            path!("/root"),
 267            json!({
 268                "a": {
 269                    "foo:bar.rs": "",
 270                    "foo.rs": "",
 271                }
 272            }),
 273        )
 274        .await;
 275
 276    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 277
 278    let (picker, _, cx) = build_find_picker(project, cx);
 279
 280    // 'foo:' matches both files
 281    cx.simulate_input("foo:");
 282    picker.update(cx, |picker, _| {
 283        assert_eq!(picker.delegate.matches.len(), 3);
 284        assert_match_at_position(picker, 0, "foo.rs");
 285        assert_match_at_position(picker, 1, "foo:bar.rs");
 286    });
 287
 288    // 'foo:b' matches one of the files
 289    cx.simulate_input("b");
 290    picker.update(cx, |picker, _| {
 291        assert_eq!(picker.delegate.matches.len(), 2);
 292        assert_match_at_position(picker, 0, "foo:bar.rs");
 293    });
 294
 295    cx.dispatch_action(editor::actions::Backspace);
 296
 297    // 'foo:1' matches both files, specifying which row to jump to
 298    cx.simulate_input("1");
 299    picker.update(cx, |picker, _| {
 300        assert_eq!(picker.delegate.matches.len(), 3);
 301        assert_match_at_position(picker, 0, "foo.rs");
 302        assert_match_at_position(picker, 1, "foo:bar.rs");
 303    });
 304}
 305
 306#[gpui::test]
 307async fn test_unicode_paths(cx: &mut TestAppContext) {
 308    let app_state = init_test(cx);
 309    app_state
 310        .fs
 311        .as_fake()
 312        .insert_tree(
 313            path!("/root"),
 314            json!({
 315                "a": {
 316                    "İg": " ",
 317                }
 318            }),
 319        )
 320        .await;
 321
 322    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 323
 324    let (picker, workspace, cx) = build_find_picker(project, cx);
 325
 326    cx.simulate_input("g");
 327    picker.update(cx, |picker, _| {
 328        assert_eq!(picker.delegate.matches.len(), 2);
 329        assert_match_at_position(picker, 1, "g");
 330    });
 331    cx.dispatch_action(Confirm);
 332    cx.read(|cx| {
 333        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 334        assert_eq!(active_editor.read(cx).title(cx), "İg");
 335    });
 336}
 337
 338#[gpui::test]
 339async fn test_absolute_paths(cx: &mut TestAppContext) {
 340    let app_state = init_test(cx);
 341    app_state
 342        .fs
 343        .as_fake()
 344        .insert_tree(
 345            path!("/root"),
 346            json!({
 347                "a": {
 348                    "file1.txt": "",
 349                    "b": {
 350                        "file2.txt": "",
 351                    },
 352                }
 353            }),
 354        )
 355        .await;
 356
 357    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 358
 359    let (picker, workspace, cx) = build_find_picker(project, cx);
 360
 361    let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
 362    picker
 363        .update_in(cx, |picker, window, cx| {
 364            picker
 365                .delegate
 366                .update_matches(matching_abs_path, window, cx)
 367        })
 368        .await;
 369    picker.update(cx, |picker, _| {
 370        assert_eq!(
 371            collect_search_matches(picker).search_paths_only(),
 372            vec![rel_path("a/b/file2.txt").into()],
 373            "Matching abs path should be the only match"
 374        )
 375    });
 376    cx.dispatch_action(SelectNext);
 377    cx.dispatch_action(Confirm);
 378    cx.read(|cx| {
 379        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 380        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
 381    });
 382
 383    let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
 384    picker
 385        .update_in(cx, |picker, window, cx| {
 386            picker
 387                .delegate
 388                .update_matches(mismatching_abs_path, window, cx)
 389        })
 390        .await;
 391    picker.update(cx, |picker, _| {
 392        assert_eq!(
 393            collect_search_matches(picker).search_paths_only(),
 394            Vec::new(),
 395            "Mismatching abs path should produce no matches"
 396        )
 397    });
 398}
 399
 400#[gpui::test]
 401async fn test_complex_path(cx: &mut TestAppContext) {
 402    let app_state = init_test(cx);
 403    app_state
 404        .fs
 405        .as_fake()
 406        .insert_tree(
 407            path!("/root"),
 408            json!({
 409                "其他": {
 410                    "S数据表格": {
 411                        "task.xlsx": "some content",
 412                    },
 413                }
 414            }),
 415        )
 416        .await;
 417
 418    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 419
 420    let (picker, workspace, cx) = build_find_picker(project, cx);
 421
 422    cx.simulate_input("t");
 423    picker.update(cx, |picker, _| {
 424        assert_eq!(picker.delegate.matches.len(), 2);
 425        assert_eq!(
 426            collect_search_matches(picker).search_paths_only(),
 427            vec![rel_path("其他/S数据表格/task.xlsx").into()],
 428        )
 429    });
 430    cx.dispatch_action(Confirm);
 431    cx.read(|cx| {
 432        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 433        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
 434    });
 435}
 436
 437#[gpui::test]
 438async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 439    let app_state = init_test(cx);
 440
 441    let first_file_name = "first.rs";
 442    let first_file_contents = "// First Rust file";
 443    app_state
 444        .fs
 445        .as_fake()
 446        .insert_tree(
 447            path!("/src"),
 448            json!({
 449                "test": {
 450                    first_file_name: first_file_contents,
 451                    "second.rs": "// Second Rust file",
 452                }
 453            }),
 454        )
 455        .await;
 456
 457    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 458
 459    let (picker, workspace, cx) = build_find_picker(project, cx);
 460
 461    let file_query = &first_file_name[..3];
 462    let file_row = 1;
 463    let file_column = 3;
 464    assert!(file_column <= first_file_contents.len());
 465    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 466    picker
 467        .update_in(cx, |finder, window, cx| {
 468            finder
 469                .delegate
 470                .update_matches(query_inside_file.to_string(), window, cx)
 471        })
 472        .await;
 473    picker.update(cx, |finder, _| {
 474        assert_match_at_position(finder, 1, &query_inside_file.to_string());
 475        let finder = &finder.delegate;
 476        assert_eq!(finder.matches.len(), 2);
 477        let latest_search_query = finder
 478            .latest_search_query
 479            .as_ref()
 480            .expect("Finder should have a query after the update_matches call");
 481        assert_eq!(latest_search_query.raw_query, query_inside_file);
 482        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 483        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 484        assert_eq!(
 485            latest_search_query.path_position.column,
 486            Some(file_column as u32)
 487        );
 488    });
 489
 490    cx.dispatch_action(Confirm);
 491
 492    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 493    cx.executor().advance_clock(Duration::from_secs(2));
 494
 495    editor.update(cx, |editor, cx| {
 496            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 497            assert_eq!(
 498                all_selections.len(),
 499                1,
 500                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 501            );
 502            let caret_selection = all_selections.into_iter().next().unwrap();
 503            assert_eq!(caret_selection.start, caret_selection.end,
 504                "Caret selection should have its start and end at the same position");
 505            assert_eq!(file_row, caret_selection.start.row + 1,
 506                "Query inside file should get caret with the same focus row");
 507            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 508                "Query inside file should get caret with the same focus column");
 509        });
 510}
 511
 512#[gpui::test]
 513async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 514    let app_state = init_test(cx);
 515
 516    let first_file_name = "first.rs";
 517    let first_file_contents = "// First Rust file";
 518    app_state
 519        .fs
 520        .as_fake()
 521        .insert_tree(
 522            path!("/src"),
 523            json!({
 524                "test": {
 525                    first_file_name: first_file_contents,
 526                    "second.rs": "// Second Rust file",
 527                }
 528            }),
 529        )
 530        .await;
 531
 532    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 533
 534    let (picker, workspace, cx) = build_find_picker(project, cx);
 535
 536    let file_query = &first_file_name[..3];
 537    let file_row = 200;
 538    let file_column = 300;
 539    assert!(file_column > first_file_contents.len());
 540    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 541    picker
 542        .update_in(cx, |picker, window, cx| {
 543            picker
 544                .delegate
 545                .update_matches(query_outside_file.to_string(), window, cx)
 546        })
 547        .await;
 548    picker.update(cx, |finder, _| {
 549        assert_match_at_position(finder, 1, &query_outside_file.to_string());
 550        let delegate = &finder.delegate;
 551        assert_eq!(delegate.matches.len(), 2);
 552        let latest_search_query = delegate
 553            .latest_search_query
 554            .as_ref()
 555            .expect("Finder should have a query after the update_matches call");
 556        assert_eq!(latest_search_query.raw_query, query_outside_file);
 557        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 558        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 559        assert_eq!(
 560            latest_search_query.path_position.column,
 561            Some(file_column as u32)
 562        );
 563    });
 564
 565    cx.dispatch_action(Confirm);
 566
 567    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 568    cx.executor().advance_clock(Duration::from_secs(2));
 569
 570    editor.update(cx, |editor, cx| {
 571            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 572            assert_eq!(
 573                all_selections.len(),
 574                1,
 575                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 576            );
 577            let caret_selection = all_selections.into_iter().next().unwrap();
 578            assert_eq!(caret_selection.start, caret_selection.end,
 579                "Caret selection should have its start and end at the same position");
 580            assert_eq!(0, caret_selection.start.row,
 581                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 582            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 583                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 584        });
 585}
 586
 587#[gpui::test]
 588async fn test_matching_cancellation(cx: &mut TestAppContext) {
 589    let app_state = init_test(cx);
 590    app_state
 591        .fs
 592        .as_fake()
 593        .insert_tree(
 594            "/dir",
 595            json!({
 596                "hello": "",
 597                "goodbye": "",
 598                "halogen-light": "",
 599                "happiness": "",
 600                "height": "",
 601                "hi": "",
 602                "hiccup": "",
 603            }),
 604        )
 605        .await;
 606
 607    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 608
 609    let (picker, _, cx) = build_find_picker(project, cx);
 610
 611    let query = test_path_position("hi");
 612    picker
 613        .update_in(cx, |picker, window, cx| {
 614            picker.delegate.spawn_search(query.clone(), window, cx)
 615        })
 616        .await;
 617
 618    picker.update(cx, |picker, _cx| {
 619        // CreateNew option not shown in this case since file already exists
 620        assert_eq!(picker.delegate.matches.len(), 5);
 621    });
 622
 623    picker.update_in(cx, |picker, window, cx| {
 624        let matches = collect_search_matches(picker).search_matches_only();
 625        let delegate = &mut picker.delegate;
 626
 627        // Simulate a search being cancelled after the time limit,
 628        // returning only a subset of the matches that would have been found.
 629        drop(delegate.spawn_search(query.clone(), window, cx));
 630        delegate.set_search_matches(
 631            delegate.latest_search_id,
 632            true, // did-cancel
 633            query.clone(),
 634            vec![
 635                ProjectPanelOrdMatch(matches[1].clone()),
 636                ProjectPanelOrdMatch(matches[3].clone()),
 637            ],
 638            cx,
 639        );
 640
 641        // Simulate another cancellation.
 642        drop(delegate.spawn_search(query.clone(), window, cx));
 643        delegate.set_search_matches(
 644            delegate.latest_search_id,
 645            true, // did-cancel
 646            query.clone(),
 647            vec![
 648                ProjectPanelOrdMatch(matches[0].clone()),
 649                ProjectPanelOrdMatch(matches[2].clone()),
 650                ProjectPanelOrdMatch(matches[3].clone()),
 651            ],
 652            cx,
 653        );
 654
 655        assert_eq!(
 656            collect_search_matches(picker)
 657                .search_matches_only()
 658                .as_slice(),
 659            &matches[0..4]
 660        );
 661    });
 662}
 663
 664#[gpui::test]
 665async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
 666    let app_state = init_test(cx);
 667    cx.update(|cx| {
 668        cx.update_global::<SettingsStore, _>(|store, cx| {
 669            store.update_user_settings(cx, |settings| {
 670                settings.project.worktree.file_scan_inclusions = Some(vec![
 671                    "height_demo/**/hi_bonjour".to_string(),
 672                    "**/height_1".to_string(),
 673                ]);
 674            });
 675        })
 676    });
 677    app_state
 678        .fs
 679        .as_fake()
 680        .insert_tree(
 681            "/ancestor",
 682            json!({
 683                ".gitignore": "ignored-root",
 684                "ignored-root": {
 685                    "happiness": "",
 686                    "height": "",
 687                    "hi": "",
 688                    "hiccup": "",
 689                },
 690                "tracked-root": {
 691                    ".gitignore": "height*",
 692                    "happiness": "",
 693                    "height": "",
 694                    "heights": {
 695                        "height_1": "",
 696                        "height_2": "",
 697                    },
 698                    "height_demo": {
 699                        "test_1": {
 700                            "hi_bonjour": "hi_bonjour",
 701                            "hi": "hello",
 702                        },
 703                        "hihi": "bye",
 704                        "test_2": {
 705                            "hoi": "nl"
 706                        }
 707                    },
 708                    "height_include": {
 709                        "height_1_include": "",
 710                        "height_2_include": "",
 711                    },
 712                    "hi": "",
 713                    "hiccup": "",
 714                },
 715            }),
 716        )
 717        .await;
 718
 719    let project = Project::test(
 720        app_state.fs.clone(),
 721        [
 722            Path::new(path!("/ancestor/tracked-root")),
 723            Path::new(path!("/ancestor/ignored-root")),
 724        ],
 725        cx,
 726    )
 727    .await;
 728    let (picker, _workspace, cx) = build_find_picker(project, cx);
 729
 730    picker
 731        .update_in(cx, |picker, window, cx| {
 732            picker
 733                .delegate
 734                .spawn_search(test_path_position("hi"), window, cx)
 735        })
 736        .await;
 737    picker.update(cx, |picker, _| {
 738        let matches = collect_search_matches(picker);
 739        assert_eq!(matches.history.len(), 0);
 740        assert_eq!(
 741            matches.search,
 742            vec![
 743                rel_path("ignored-root/hi").into(),
 744                rel_path("tracked-root/hi").into(),
 745                rel_path("ignored-root/hiccup").into(),
 746                rel_path("tracked-root/hiccup").into(),
 747                rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(),
 748                rel_path("ignored-root/height").into(),
 749                rel_path("tracked-root/heights/height_1").into(),
 750                rel_path("ignored-root/happiness").into(),
 751                rel_path("tracked-root/happiness").into(),
 752            ],
 753            "All ignored files that were indexed are found for default ignored mode"
 754        );
 755    });
 756}
 757
 758#[gpui::test]
 759async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
 760    let app_state = init_test(cx);
 761    cx.update(|cx| {
 762        cx.update_global::<SettingsStore, _>(|store, cx| {
 763            store.update_user_settings(cx, |settings| {
 764                settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
 765            });
 766        })
 767    });
 768    app_state
 769        .fs
 770        .as_fake()
 771        .insert_tree(
 772            "/src",
 773            json!({
 774                ".gitignore": "node_modules",
 775                "node_modules": {
 776                    "package.json": "// package.json",
 777                    ".env": "BAR=FOO"
 778                },
 779                ".env": "FOO=BAR"
 780            }),
 781        )
 782        .await;
 783
 784    let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await;
 785    let (picker, _workspace, cx) = build_find_picker(project, cx);
 786
 787    picker
 788        .update_in(cx, |picker, window, cx| {
 789            picker
 790                .delegate
 791                .spawn_search(test_path_position("json"), window, cx)
 792        })
 793        .await;
 794    picker.update(cx, |picker, _| {
 795        let matches = collect_search_matches(picker);
 796        assert_eq!(matches.history.len(), 0);
 797        assert_eq!(
 798            matches.search,
 799            vec![],
 800            "All ignored files that were indexed are found for default ignored mode"
 801        );
 802    });
 803}
 804
 805#[gpui::test]
 806async fn test_ignored_root(cx: &mut TestAppContext) {
 807    let app_state = init_test(cx);
 808    app_state
 809        .fs
 810        .as_fake()
 811        .insert_tree(
 812            "/ancestor",
 813            json!({
 814                ".gitignore": "ignored-root",
 815                "ignored-root": {
 816                    "happiness": "",
 817                    "height": "",
 818                    "hi": "",
 819                    "hiccup": "",
 820                },
 821                "tracked-root": {
 822                    ".gitignore": "height*",
 823                    "happiness": "",
 824                    "height": "",
 825                    "heights": {
 826                        "height_1": "",
 827                        "height_2": "",
 828                    },
 829                    "hi": "",
 830                    "hiccup": "",
 831                },
 832            }),
 833        )
 834        .await;
 835
 836    let project = Project::test(
 837        app_state.fs.clone(),
 838        [
 839            Path::new(path!("/ancestor/tracked-root")),
 840            Path::new(path!("/ancestor/ignored-root")),
 841        ],
 842        cx,
 843    )
 844    .await;
 845    let (picker, workspace, cx) = build_find_picker(project, cx);
 846
 847    picker
 848        .update_in(cx, |picker, window, cx| {
 849            picker
 850                .delegate
 851                .spawn_search(test_path_position("hi"), window, cx)
 852        })
 853        .await;
 854    picker.update(cx, |picker, _| {
 855        let matches = collect_search_matches(picker);
 856        assert_eq!(matches.history.len(), 0);
 857        assert_eq!(
 858            matches.search,
 859            vec![
 860                rel_path("ignored-root/hi").into(),
 861                rel_path("tracked-root/hi").into(),
 862                rel_path("ignored-root/hiccup").into(),
 863                rel_path("tracked-root/hiccup").into(),
 864                rel_path("ignored-root/height").into(),
 865                rel_path("ignored-root/happiness").into(),
 866                rel_path("tracked-root/happiness").into(),
 867            ],
 868            "All ignored files that were indexed are found for default ignored mode"
 869        );
 870    });
 871    cx.dispatch_action(ToggleIncludeIgnored);
 872    picker
 873        .update_in(cx, |picker, window, cx| {
 874            picker
 875                .delegate
 876                .spawn_search(test_path_position("hi"), window, cx)
 877        })
 878        .await;
 879    picker.update(cx, |picker, _| {
 880        let matches = collect_search_matches(picker);
 881        assert_eq!(matches.history.len(), 0);
 882        assert_eq!(
 883            matches.search,
 884            vec![
 885                rel_path("ignored-root/hi").into(),
 886                rel_path("tracked-root/hi").into(),
 887                rel_path("ignored-root/hiccup").into(),
 888                rel_path("tracked-root/hiccup").into(),
 889                rel_path("ignored-root/height").into(),
 890                rel_path("tracked-root/height").into(),
 891                rel_path("ignored-root/happiness").into(),
 892                rel_path("tracked-root/happiness").into(),
 893            ],
 894            "All ignored files should be found, for the toggled on ignored mode"
 895        );
 896    });
 897
 898    picker
 899        .update_in(cx, |picker, window, cx| {
 900            picker.delegate.include_ignored = Some(false);
 901            picker
 902                .delegate
 903                .spawn_search(test_path_position("hi"), window, cx)
 904        })
 905        .await;
 906    picker.update(cx, |picker, _| {
 907        let matches = collect_search_matches(picker);
 908        assert_eq!(matches.history.len(), 0);
 909        assert_eq!(
 910            matches.search,
 911            vec![
 912                rel_path("tracked-root/hi").into(),
 913                rel_path("tracked-root/hiccup").into(),
 914                rel_path("tracked-root/happiness").into(),
 915            ],
 916            "Only non-ignored files should be found for the turned off ignored mode"
 917        );
 918    });
 919
 920    workspace
 921        .update_in(cx, |workspace, window, cx| {
 922            workspace.open_abs_path(
 923                PathBuf::from(path!("/ancestor/tracked-root/heights/height_1")),
 924                OpenOptions {
 925                    visible: Some(OpenVisible::None),
 926                    ..OpenOptions::default()
 927                },
 928                window,
 929                cx,
 930            )
 931        })
 932        .await
 933        .unwrap();
 934    cx.run_until_parked();
 935    workspace
 936        .update_in(cx, |workspace, window, cx| {
 937            workspace.active_pane().update(cx, |pane, cx| {
 938                pane.close_active_item(&CloseActiveItem::default(), window, cx)
 939            })
 940        })
 941        .await
 942        .unwrap();
 943    cx.run_until_parked();
 944
 945    picker
 946        .update_in(cx, |picker, window, cx| {
 947            picker.delegate.include_ignored = None;
 948            picker
 949                .delegate
 950                .spawn_search(test_path_position("hi"), window, cx)
 951        })
 952        .await;
 953    picker.update(cx, |picker, _| {
 954        let matches = collect_search_matches(picker);
 955        assert_eq!(matches.history.len(), 0);
 956        assert_eq!(
 957            matches.search,
 958            vec![
 959                rel_path("ignored-root/hi").into(),
 960                rel_path("tracked-root/hi").into(),
 961                rel_path("ignored-root/hiccup").into(),
 962                rel_path("tracked-root/hiccup").into(),
 963                rel_path("ignored-root/height").into(),
 964                rel_path("ignored-root/happiness").into(),
 965                rel_path("tracked-root/happiness").into(),
 966            ],
 967            "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
 968        );
 969    });
 970
 971    picker
 972        .update_in(cx, |picker, window, cx| {
 973            picker.delegate.include_ignored = Some(true);
 974            picker
 975                .delegate
 976                .spawn_search(test_path_position("hi"), window, cx)
 977        })
 978        .await;
 979    picker.update(cx, |picker, _| {
 980        let matches = collect_search_matches(picker);
 981        assert_eq!(matches.history.len(), 0);
 982        assert_eq!(
 983            matches.search,
 984            vec![
 985                rel_path("ignored-root/hi").into(),
 986                rel_path("tracked-root/hi").into(),
 987                rel_path("ignored-root/hiccup").into(),
 988                rel_path("tracked-root/hiccup").into(),
 989                rel_path("ignored-root/height").into(),
 990                rel_path("tracked-root/height").into(),
 991                rel_path("tracked-root/heights/height_1").into(),
 992                rel_path("tracked-root/heights/height_2").into(),
 993                rel_path("ignored-root/happiness").into(),
 994                rel_path("tracked-root/happiness").into(),
 995            ],
 996            "All ignored files that were indexed are found in the turned on ignored mode"
 997        );
 998    });
 999
1000    picker
1001        .update_in(cx, |picker, window, cx| {
1002            picker.delegate.include_ignored = Some(false);
1003            picker
1004                .delegate
1005                .spawn_search(test_path_position("hi"), window, cx)
1006        })
1007        .await;
1008    picker.update(cx, |picker, _| {
1009        let matches = collect_search_matches(picker);
1010        assert_eq!(matches.history.len(), 0);
1011        assert_eq!(
1012            matches.search,
1013            vec![
1014                rel_path("tracked-root/hi").into(),
1015                rel_path("tracked-root/hiccup").into(),
1016                rel_path("tracked-root/happiness").into(),
1017            ],
1018            "Only non-ignored files should be found for the turned off ignored mode"
1019        );
1020    });
1021}
1022
1023#[gpui::test]
1024async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1025    let app_state = init_test(cx);
1026    app_state
1027        .fs
1028        .as_fake()
1029        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1030        .await;
1031
1032    let project = Project::test(
1033        app_state.fs.clone(),
1034        ["/root/the-parent-dir/the-file".as_ref()],
1035        cx,
1036    )
1037    .await;
1038
1039    let (picker, _, cx) = build_find_picker(project, cx);
1040
1041    // Even though there is only one worktree, that worktree's filename
1042    // is included in the matching, because the worktree is a single file.
1043    picker
1044        .update_in(cx, |picker, window, cx| {
1045            picker
1046                .delegate
1047                .spawn_search(test_path_position("thf"), window, cx)
1048        })
1049        .await;
1050    cx.read(|cx| {
1051        let picker = picker.read(cx);
1052        let delegate = &picker.delegate;
1053        let matches = collect_search_matches(picker).search_matches_only();
1054        assert_eq!(matches.len(), 1);
1055
1056        let (file_name, file_name_positions, full_path, full_path_positions) =
1057            delegate.labels_for_path_match(&matches[0], PathStyle::local());
1058        assert_eq!(file_name, "the-file");
1059        assert_eq!(file_name_positions, &[0, 1, 4]);
1060        assert_eq!(full_path, "");
1061        assert_eq!(full_path_positions, &[0; 0]);
1062    });
1063
1064    // Since the worktree root is a file, searching for its name followed by a slash does
1065    // not match anything.
1066    picker
1067        .update_in(cx, |picker, window, cx| {
1068            picker
1069                .delegate
1070                .spawn_search(test_path_position("thf/"), window, cx)
1071        })
1072        .await;
1073    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1074}
1075
1076#[gpui::test]
1077async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppContext) {
1078    let app_state = init_test(cx);
1079    app_state
1080        .fs
1081        .as_fake()
1082        .insert_tree(
1083            path!("/repo1"),
1084            json!({
1085                "package.json": r#"{"name": "repo1"}"#,
1086                "src": {
1087                    "index.js": "// Repo 1 index",
1088                }
1089            }),
1090        )
1091        .await;
1092
1093    app_state
1094        .fs
1095        .as_fake()
1096        .insert_tree(
1097            path!("/repo2"),
1098            json!({
1099                "package.json": r#"{"name": "repo2"}"#,
1100                "src": {
1101                    "index.js": "// Repo 2 index",
1102                }
1103            }),
1104        )
1105        .await;
1106
1107    let project = Project::test(
1108        app_state.fs.clone(),
1109        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
1110        cx,
1111    )
1112    .await;
1113
1114    let (multi_workspace, cx) =
1115        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1116    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1117    let (worktree_id1, worktree_id2) = cx.read(|cx| {
1118        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1119        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1120    });
1121
1122    workspace
1123        .update_in(cx, |workspace, window, cx| {
1124            workspace.open_path(
1125                ProjectPath {
1126                    worktree_id: worktree_id1,
1127                    path: rel_path("package.json").into(),
1128                },
1129                None,
1130                true,
1131                window,
1132                cx,
1133            )
1134        })
1135        .await
1136        .unwrap();
1137
1138    cx.dispatch_action(workspace::CloseActiveItem {
1139        save_intent: None,
1140        close_pinned: false,
1141    });
1142
1143    let picker = open_file_picker(&workspace, cx);
1144    cx.simulate_input("package.json");
1145
1146    picker.update(cx, |finder, _| {
1147        let matches = &finder.delegate.matches.matches;
1148
1149        assert_eq!(
1150            matches.len(),
1151            2,
1152            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
1153            matches.len(),
1154            matches
1155        );
1156
1157        assert_matches!(matches[0], Match::History { .. });
1158
1159        let search_matches = collect_search_matches(finder);
1160        assert_eq!(
1161            search_matches.history.len(),
1162            1,
1163            "Should have exactly 1 history match"
1164        );
1165        assert_eq!(
1166            search_matches.search.len(),
1167            1,
1168            "Should have exactly 1 search match (the other package.json)"
1169        );
1170
1171        if let Match::History { path, .. } = &matches[0] {
1172            assert_eq!(path.project.worktree_id, worktree_id1);
1173            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
1174        }
1175
1176        if let Match::Search(path_match) = &matches[1] {
1177            assert_eq!(
1178                WorktreeId::from_usize(path_match.0.worktree_id),
1179                worktree_id2
1180            );
1181            assert_eq!(path_match.0.path.as_ref(), rel_path("package.json"));
1182        }
1183    });
1184}
1185
1186#[gpui::test]
1187async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
1188    let app_state = init_test(cx);
1189    app_state
1190        .fs
1191        .as_fake()
1192        .insert_tree(
1193            path!("/roota"),
1194            json!({ "the-parent-dira": { "filea": "" } }),
1195        )
1196        .await;
1197
1198    app_state
1199        .fs
1200        .as_fake()
1201        .insert_tree(
1202            path!("/rootb"),
1203            json!({ "the-parent-dirb": { "fileb": "" } }),
1204        )
1205        .await;
1206
1207    let project = Project::test(
1208        app_state.fs.clone(),
1209        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1210        cx,
1211    )
1212    .await;
1213
1214    let (multi_workspace, cx) =
1215        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1216    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1217    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1218        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1219        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1220    });
1221
1222    let b_path = ProjectPath {
1223        worktree_id: worktree_id2,
1224        path: rel_path("the-parent-dirb/fileb").into(),
1225    };
1226    workspace
1227        .update_in(cx, |workspace, window, cx| {
1228            workspace.open_path(b_path, None, true, window, cx)
1229        })
1230        .await
1231        .unwrap();
1232
1233    let finder = open_file_picker(&workspace, cx);
1234
1235    finder
1236        .update_in(cx, |f, window, cx| {
1237            f.delegate.spawn_search(
1238                test_path_position(path!("the-parent-dirb/filec")),
1239                window,
1240                cx,
1241            )
1242        })
1243        .await;
1244    cx.run_until_parked();
1245    finder.update_in(cx, |picker, window, cx| {
1246        assert_eq!(picker.delegate.matches.len(), 1);
1247        picker.delegate.confirm(false, window, cx)
1248    });
1249    cx.run_until_parked();
1250    cx.read(|cx| {
1251        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1252        let project_path = active_editor.read(cx).project_path(cx);
1253        assert_eq!(
1254            project_path,
1255            Some(ProjectPath {
1256                worktree_id: worktree_id2,
1257                path: rel_path("the-parent-dirb/filec").into()
1258            })
1259        );
1260    });
1261}
1262
1263#[gpui::test]
1264async fn test_create_file_focused_file_does_not_belong_to_available_worktrees(
1265    cx: &mut TestAppContext,
1266) {
1267    let app_state = init_test(cx);
1268    app_state
1269        .fs
1270        .as_fake()
1271        .insert_tree(path!("/roota"), json!({ "the-parent-dira": { "filea": ""}}))
1272        .await;
1273
1274    app_state
1275        .fs
1276        .as_fake()
1277        .insert_tree(path!("/rootb"), json!({"the-parent-dirb":{ "fileb": ""}}))
1278        .await;
1279
1280    let project = Project::test(
1281        app_state.fs.clone(),
1282        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1283        cx,
1284    )
1285    .await;
1286
1287    let (multi_workspace, cx) =
1288        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1289    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1290
1291    let (worktree_id_a, worktree_id_b) = cx.read(|cx| {
1292        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1293        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1294    });
1295    workspace
1296        .update_in(cx, |workspace, window, cx| {
1297            workspace.open_abs_path(
1298                PathBuf::from(path!("/external/external-file.txt")),
1299                OpenOptions {
1300                    visible: Some(OpenVisible::None),
1301                    ..OpenOptions::default()
1302                },
1303                window,
1304                cx,
1305            )
1306        })
1307        .await
1308        .unwrap();
1309
1310    cx.run_until_parked();
1311    let finder = open_file_picker(&workspace, cx);
1312
1313    finder
1314        .update_in(cx, |f, window, cx| {
1315            f.delegate
1316                .spawn_search(test_path_position("new-file.txt"), window, cx)
1317        })
1318        .await;
1319
1320    cx.run_until_parked();
1321    finder.update_in(cx, |f, window, cx| {
1322        assert_eq!(f.delegate.matches.len(), 1);
1323        f.delegate.confirm(false, window, cx); // ✓ works
1324    });
1325    cx.run_until_parked();
1326
1327    cx.read(|cx| {
1328        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1329
1330        let project_path = active_editor.read(cx).project_path(cx);
1331
1332        assert!(
1333            project_path.is_some(),
1334            "Active editor should have a project path"
1335        );
1336
1337        let project_path = project_path.unwrap();
1338
1339        assert!(
1340            project_path.worktree_id == worktree_id_a || project_path.worktree_id == worktree_id_b,
1341            "New file should be created in one of the available worktrees (A or B), \
1342                not in a directory derived from the external file. Got worktree_id: {:?}",
1343            project_path.worktree_id
1344        );
1345
1346        assert_eq!(project_path.path.as_ref(), rel_path("new-file.txt"));
1347    });
1348}
1349
1350#[gpui::test]
1351async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppContext) {
1352    let app_state = init_test(cx);
1353    app_state
1354        .fs
1355        .as_fake()
1356        .insert_tree(
1357            path!("/roota"),
1358            json!({ "the-parent-dira": { "filea": "" } }),
1359        )
1360        .await;
1361
1362    app_state
1363        .fs
1364        .as_fake()
1365        .insert_tree(
1366            path!("/rootb"),
1367            json!({ "the-parent-dirb": { "fileb": "" } }),
1368        )
1369        .await;
1370
1371    let project = Project::test(
1372        app_state.fs.clone(),
1373        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1374        cx,
1375    )
1376    .await;
1377
1378    let (multi_workspace, cx) =
1379        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1380    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1381    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1382        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1383        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1384    });
1385
1386    let finder = open_file_picker(&workspace, cx);
1387
1388    finder
1389        .update_in(cx, |f, window, cx| {
1390            f.delegate
1391                .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1392        })
1393        .await;
1394    cx.run_until_parked();
1395    finder.update_in(cx, |picker, window, cx| {
1396        assert_eq!(picker.delegate.matches.len(), 1);
1397        picker.delegate.confirm(false, window, cx)
1398    });
1399    cx.run_until_parked();
1400    cx.read(|cx| {
1401        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1402        let project_path = active_editor.read(cx).project_path(cx);
1403        assert_eq!(
1404            project_path,
1405            Some(ProjectPath {
1406                worktree_id: worktree_id2,
1407                path: rel_path("filec").into()
1408            })
1409        );
1410    });
1411}
1412
1413#[gpui::test]
1414async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1415    let app_state = init_test(cx);
1416    app_state
1417        .fs
1418        .as_fake()
1419        .insert_tree(
1420            path!("/root"),
1421            json!({
1422                "dir1": { "a.txt": "" },
1423                "dir2": {
1424                    "a.txt": "",
1425                    "b.txt": ""
1426                }
1427            }),
1428        )
1429        .await;
1430
1431    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1432    let (multi_workspace, cx) =
1433        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1434    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1435
1436    let worktree_id = cx.read(|cx| {
1437        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1438        assert_eq!(worktrees.len(), 1);
1439        worktrees[0].read(cx).id()
1440    });
1441
1442    // When workspace has an active item, sort items which are closer to that item
1443    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1444    // so that one should be sorted earlier
1445    let b_path = ProjectPath {
1446        worktree_id,
1447        path: rel_path("dir2/b.txt").into(),
1448    };
1449    workspace
1450        .update_in(cx, |workspace, window, cx| {
1451            workspace.open_path(b_path, None, true, window, cx)
1452        })
1453        .await
1454        .unwrap();
1455    let finder = open_file_picker(&workspace, cx);
1456    finder
1457        .update_in(cx, |f, window, cx| {
1458            f.delegate
1459                .spawn_search(test_path_position("a.txt"), window, cx)
1460        })
1461        .await;
1462
1463    finder.update(cx, |picker, _| {
1464        let matches = collect_search_matches(picker).search_paths_only();
1465        assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1466        assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1467    });
1468}
1469
1470#[gpui::test]
1471async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1472    let app_state = init_test(cx);
1473    app_state
1474        .fs
1475        .as_fake()
1476        .insert_tree(
1477            "/root",
1478            json!({
1479                "dir1": {},
1480                "dir2": {
1481                    "dir3": {}
1482                }
1483            }),
1484        )
1485        .await;
1486
1487    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1488    let (picker, _workspace, cx) = build_find_picker(project, cx);
1489
1490    picker
1491        .update_in(cx, |f, window, cx| {
1492            f.delegate
1493                .spawn_search(test_path_position("dir"), window, cx)
1494        })
1495        .await;
1496    cx.read(|cx| {
1497        let finder = picker.read(cx);
1498        assert_eq!(finder.delegate.matches.len(), 1);
1499        assert_match_at_position(finder, 0, "dir");
1500    });
1501}
1502
1503#[gpui::test]
1504async fn test_query_history(cx: &mut gpui::TestAppContext) {
1505    let app_state = init_test(cx);
1506
1507    app_state
1508        .fs
1509        .as_fake()
1510        .insert_tree(
1511            path!("/src"),
1512            json!({
1513                "test": {
1514                    "first.rs": "// First Rust file",
1515                    "second.rs": "// Second Rust file",
1516                    "third.rs": "// Third Rust file",
1517                }
1518            }),
1519        )
1520        .await;
1521
1522    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1523    let (multi_workspace, cx) =
1524        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1525    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1526    let worktree_id = cx.read(|cx| {
1527        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1528        assert_eq!(worktrees.len(), 1);
1529        worktrees[0].read(cx).id()
1530    });
1531
1532    // Open and close panels, getting their history items afterwards.
1533    // Ensure history items get populated with opened items, and items are kept in a certain order.
1534    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1535    //
1536    // TODO: without closing, the opened items do not propagate their history changes for some reason
1537    // it does work in real app though, only tests do not propagate.
1538    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1539
1540    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1541    assert!(
1542        initial_history.is_empty(),
1543        "Should have no history before opening any files"
1544    );
1545
1546    let history_after_first =
1547        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1548    assert_eq!(
1549        history_after_first,
1550        vec![FoundPath::new(
1551            ProjectPath {
1552                worktree_id,
1553                path: rel_path("test/first.rs").into(),
1554            },
1555            PathBuf::from(path!("/src/test/first.rs"))
1556        )],
1557        "Should show 1st opened item in the history when opening the 2nd item"
1558    );
1559
1560    let history_after_second =
1561        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1562    assert_eq!(
1563        history_after_second,
1564        vec![
1565            FoundPath::new(
1566                ProjectPath {
1567                    worktree_id,
1568                    path: rel_path("test/second.rs").into(),
1569                },
1570                PathBuf::from(path!("/src/test/second.rs"))
1571            ),
1572            FoundPath::new(
1573                ProjectPath {
1574                    worktree_id,
1575                    path: rel_path("test/first.rs").into(),
1576                },
1577                PathBuf::from(path!("/src/test/first.rs"))
1578            ),
1579        ],
1580        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1581    2nd item should be the first in the history, as the last opened."
1582    );
1583
1584    let history_after_third =
1585        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1586    assert_eq!(
1587        history_after_third,
1588        vec![
1589            FoundPath::new(
1590                ProjectPath {
1591                    worktree_id,
1592                    path: rel_path("test/third.rs").into(),
1593                },
1594                PathBuf::from(path!("/src/test/third.rs"))
1595            ),
1596            FoundPath::new(
1597                ProjectPath {
1598                    worktree_id,
1599                    path: rel_path("test/second.rs").into(),
1600                },
1601                PathBuf::from(path!("/src/test/second.rs"))
1602            ),
1603            FoundPath::new(
1604                ProjectPath {
1605                    worktree_id,
1606                    path: rel_path("test/first.rs").into(),
1607                },
1608                PathBuf::from(path!("/src/test/first.rs"))
1609            ),
1610        ],
1611        "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1612    3rd item should be the first in the history, as the last opened."
1613    );
1614
1615    let history_after_second_again =
1616        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1617    assert_eq!(
1618        history_after_second_again,
1619        vec![
1620            FoundPath::new(
1621                ProjectPath {
1622                    worktree_id,
1623                    path: rel_path("test/second.rs").into(),
1624                },
1625                PathBuf::from(path!("/src/test/second.rs"))
1626            ),
1627            FoundPath::new(
1628                ProjectPath {
1629                    worktree_id,
1630                    path: rel_path("test/third.rs").into(),
1631                },
1632                PathBuf::from(path!("/src/test/third.rs"))
1633            ),
1634            FoundPath::new(
1635                ProjectPath {
1636                    worktree_id,
1637                    path: rel_path("test/first.rs").into(),
1638                },
1639                PathBuf::from(path!("/src/test/first.rs"))
1640            ),
1641        ],
1642        "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1643    2nd item, as the last opened, 3rd item should go next as it was opened right before."
1644    );
1645}
1646
1647#[gpui::test]
1648async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1649    let app_state = init_test(cx);
1650
1651    app_state
1652        .fs
1653        .as_fake()
1654        .insert_tree(
1655            path!("/src"),
1656            json!({
1657                "test": {
1658                    "first.rs": "// First Rust file",
1659                    "second.rs": "// Second Rust file",
1660                    "third.rs": "// Third Rust file",
1661                }
1662            }),
1663        )
1664        .await;
1665
1666    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1667    let (multi_workspace, cx) =
1668        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1669    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1670
1671    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1672
1673    open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
1674    let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
1675    assert_eq!(history.len(), 1);
1676
1677    let picker = open_file_picker(&workspace, cx);
1678    cx.simulate_input("fir");
1679    picker.update_in(cx, |finder, window, cx| {
1680        let matches = &finder.delegate.matches.matches;
1681        assert_matches!(
1682            matches.as_slice(),
1683            [Match::History { .. }, Match::CreateNew { .. }]
1684        );
1685        assert_eq!(
1686            matches[0].panel_match().unwrap().0.path.as_ref(),
1687            rel_path("test/first.rs")
1688        );
1689        assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
1690
1691        let (file_label, path_label) =
1692            finder
1693                .delegate
1694                .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
1695        assert_eq!(file_label.text(), "first.rs");
1696        assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
1697        assert_eq!(
1698            path_label.text(),
1699            format!("test{}", PathStyle::local().primary_separator())
1700        );
1701        assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
1702    });
1703}
1704
1705#[gpui::test]
1706async fn test_history_labels_do_not_include_worktree_root_name(cx: &mut gpui::TestAppContext) {
1707    let app_state = init_test(cx);
1708
1709    cx.update(|cx| {
1710        let settings = *ProjectPanelSettings::get_global(cx);
1711        ProjectPanelSettings::override_global(
1712            ProjectPanelSettings {
1713                hide_root: true,
1714                ..settings
1715            },
1716            cx,
1717        );
1718    });
1719
1720    app_state
1721        .fs
1722        .as_fake()
1723        .insert_tree(
1724            path!("/my_project"),
1725            json!({
1726                "src": {
1727                    "first.rs": "// First Rust file",
1728                    "second.rs": "// Second Rust file",
1729                }
1730            }),
1731        )
1732        .await;
1733
1734    let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await;
1735    let (multi_workspace, cx) =
1736        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1737    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1738
1739    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1740    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1741
1742    let picker = open_file_picker(&workspace, cx);
1743    picker.update_in(cx, |finder, window, cx| {
1744        let matches = &finder.delegate.matches.matches;
1745        assert!(matches.len() >= 2);
1746
1747        for m in matches.iter() {
1748            if let Match::History { panel_match, .. } = m {
1749                assert!(
1750                    panel_match.is_none(),
1751                    "History items with no query should not have a panel match"
1752                );
1753            }
1754        }
1755
1756        let separator = PathStyle::local().primary_separator();
1757
1758        let (file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx);
1759        assert_eq!(file_label.text(), "second.rs");
1760        assert_eq!(
1761            path_label.text(),
1762            format!("src{separator}"),
1763            "History path label must not contain root name 'my_project'"
1764        );
1765
1766        let (file_label, path_label) = finder.delegate.labels_for_match(&matches[1], window, cx);
1767        assert_eq!(file_label.text(), "first.rs");
1768        assert_eq!(
1769            path_label.text(),
1770            format!("src{separator}"),
1771            "History path label must not contain root name 'my_project'"
1772        );
1773    });
1774
1775    // Now type a query so history items get panel_match populated,
1776    // and verify labels stay consistent with the no-query case.
1777    let picker = active_file_picker(&workspace, cx);
1778    picker
1779        .update_in(cx, |finder, window, cx| {
1780            finder
1781                .delegate
1782                .update_matches("first".to_string(), window, cx)
1783        })
1784        .await;
1785    picker.update_in(cx, |finder, window, cx| {
1786        let matches = &finder.delegate.matches.matches;
1787        let history_match = matches
1788            .iter()
1789            .find(|m| matches!(m, Match::History { .. }))
1790            .expect("Should have a history match for 'first'");
1791
1792        let (file_label, path_label) = finder.delegate.labels_for_match(history_match, window, cx);
1793        assert_eq!(file_label.text(), "first.rs");
1794        let separator = PathStyle::local().primary_separator();
1795        assert_eq!(
1796            path_label.text(),
1797            format!("src{separator}"),
1798            "Queried history path label must not contain root name 'my_project'"
1799        );
1800    });
1801}
1802
1803#[gpui::test]
1804async fn test_history_labels_include_worktree_root_name_when_hide_root_false(
1805    cx: &mut gpui::TestAppContext,
1806) {
1807    let app_state = init_test(cx);
1808
1809    cx.update(|cx| {
1810        let settings = *ProjectPanelSettings::get_global(cx);
1811        ProjectPanelSettings::override_global(
1812            ProjectPanelSettings {
1813                hide_root: false,
1814                ..settings
1815            },
1816            cx,
1817        );
1818    });
1819
1820    app_state
1821        .fs
1822        .as_fake()
1823        .insert_tree(
1824            path!("/my_project"),
1825            json!({
1826                "src": {
1827                    "first.rs": "// First Rust file",
1828                    "second.rs": "// Second Rust file",
1829                }
1830            }),
1831        )
1832        .await;
1833
1834    let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await;
1835    let (multi_workspace, cx) =
1836        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1837    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1838
1839    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1840    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1841
1842    let picker = open_file_picker(&workspace, cx);
1843    picker.update_in(cx, |finder, window, cx| {
1844        let matches = &finder.delegate.matches.matches;
1845        let separator = PathStyle::local().primary_separator();
1846
1847        let (_file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx);
1848        assert_eq!(
1849            path_label.text(),
1850            format!("my_project{separator}src{separator}"),
1851            "With hide_root=false, history path label should include root name 'my_project'"
1852        );
1853    });
1854}
1855
1856#[gpui::test]
1857async fn test_history_labels_include_worktree_root_name_when_hide_root_true_and_multiple_folders(
1858    cx: &mut gpui::TestAppContext,
1859) {
1860    let app_state = init_test(cx);
1861
1862    cx.update(|cx| {
1863        let settings = *ProjectPanelSettings::get_global(cx);
1864        ProjectPanelSettings::override_global(
1865            ProjectPanelSettings {
1866                hide_root: true,
1867                ..settings
1868            },
1869            cx,
1870        );
1871    });
1872
1873    app_state
1874        .fs
1875        .as_fake()
1876        .insert_tree(
1877            path!("/my_project"),
1878            json!({
1879                "src": {
1880                    "first.rs": "// First Rust file",
1881                    "second.rs": "// Second Rust file",
1882                }
1883            }),
1884        )
1885        .await;
1886
1887    app_state
1888        .fs
1889        .as_fake()
1890        .insert_tree(
1891            path!("/my_second_project"),
1892            json!({
1893                "src": {
1894                    "third.rs": "// Third Rust file",
1895                    "fourth.rs": "// Fourth Rust file",
1896                }
1897            }),
1898        )
1899        .await;
1900
1901    let project = Project::test(
1902        app_state.fs.clone(),
1903        [
1904            path!("/my_project").as_ref(),
1905            path!("/my_second_project").as_ref(),
1906        ],
1907        cx,
1908    )
1909    .await;
1910    let (multi_workspace, cx) =
1911        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1912    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1913
1914    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1915    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1916
1917    let picker = open_file_picker(&workspace, cx);
1918    picker.update_in(cx, |finder, window, cx| {
1919        let matches = &finder.delegate.matches.matches;
1920        assert!(matches.len() >= 2, "Should have at least 2 history matches");
1921
1922        let separator = PathStyle::local().primary_separator();
1923
1924        let first_match = matches
1925            .iter()
1926            .find(|m| {
1927                if let Match::History { path, .. } = m {
1928                    path.project.path.file_name()
1929                        .map(|n| n.to_string())
1930                        .map_or(false, |name| name == "first.rs")
1931                } else {
1932                    false
1933                }
1934            })
1935            .expect("Should have history match for first.rs");
1936
1937        let third_match = matches
1938            .iter()
1939            .find(|m| {
1940                if let Match::History { path, .. } = m {
1941                    path.project.path.file_name()
1942                        .map(|n| n.to_string())
1943                        .map_or(false, |name| name == "third.rs")
1944                } else {
1945                    false
1946                }
1947            })
1948            .expect("Should have history match for third.rs");
1949
1950        let (_file_label, path_label) =
1951            finder.delegate.labels_for_match(first_match, window, cx);
1952        assert_eq!(
1953            path_label.text(),
1954            format!("my_project{separator}src{separator}"),
1955            "With hide_root=true and multiple folders, history path label should include root name 'my_project'"
1956        );
1957
1958        let (_file_label, path_label) =
1959            finder.delegate.labels_for_match(third_match, window, cx);
1960        assert_eq!(
1961            path_label.text(),
1962            format!("my_second_project{separator}src{separator}"),
1963            "With hide_root=true and multiple folders, history path label should include root name 'my_second_project'"
1964        );
1965    });
1966}
1967
1968#[gpui::test]
1969async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1970    let app_state = init_test(cx);
1971
1972    app_state
1973        .fs
1974        .as_fake()
1975        .insert_tree(
1976            path!("/src"),
1977            json!({
1978                "test": {
1979                    "first.rs": "// First Rust file",
1980                    "second.rs": "// Second Rust file",
1981                }
1982            }),
1983        )
1984        .await;
1985
1986    app_state
1987        .fs
1988        .as_fake()
1989        .insert_tree(
1990            path!("/external-src"),
1991            json!({
1992                "test": {
1993                    "third.rs": "// Third Rust file",
1994                    "fourth.rs": "// Fourth Rust file",
1995                }
1996            }),
1997        )
1998        .await;
1999
2000    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2001    cx.update(|cx| {
2002        project.update(cx, |project, cx| {
2003            project.find_or_create_worktree(path!("/external-src"), false, cx)
2004        })
2005    })
2006    .detach();
2007    cx.background_executor.run_until_parked();
2008
2009    let (multi_workspace, cx) =
2010        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2011    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2012    let worktree_id = cx.read(|cx| {
2013        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2014        assert_eq!(worktrees.len(), 1,);
2015
2016        worktrees[0].read(cx).id()
2017    });
2018    workspace
2019        .update_in(cx, |workspace, window, cx| {
2020            workspace.open_abs_path(
2021                PathBuf::from(path!("/external-src/test/third.rs")),
2022                OpenOptions {
2023                    visible: Some(OpenVisible::None),
2024                    ..Default::default()
2025                },
2026                window,
2027                cx,
2028            )
2029        })
2030        .detach();
2031    cx.background_executor.run_until_parked();
2032    let external_worktree_id = cx.read(|cx| {
2033        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2034        assert_eq!(
2035            worktrees.len(),
2036            2,
2037            "External file should get opened in a new worktree"
2038        );
2039
2040        worktrees
2041            .into_iter()
2042            .find(|worktree| worktree.read(cx).id() != worktree_id)
2043            .expect("New worktree should have a different id")
2044            .read(cx)
2045            .id()
2046    });
2047    cx.dispatch_action(workspace::CloseActiveItem {
2048        save_intent: None,
2049        close_pinned: false,
2050    });
2051
2052    let initial_history_items =
2053        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2054    assert_eq!(
2055        initial_history_items,
2056        vec![FoundPath::new(
2057            ProjectPath {
2058                worktree_id: external_worktree_id,
2059                path: rel_path("").into(),
2060            },
2061            PathBuf::from(path!("/external-src/test/third.rs"))
2062        )],
2063        "Should show external file with its full path in the history after it was open"
2064    );
2065
2066    let updated_history_items =
2067        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2068    assert_eq!(
2069        updated_history_items,
2070        vec![
2071            FoundPath::new(
2072                ProjectPath {
2073                    worktree_id,
2074                    path: rel_path("test/second.rs").into(),
2075                },
2076                PathBuf::from(path!("/src/test/second.rs"))
2077            ),
2078            FoundPath::new(
2079                ProjectPath {
2080                    worktree_id: external_worktree_id,
2081                    path: rel_path("").into(),
2082                },
2083                PathBuf::from(path!("/external-src/test/third.rs"))
2084            ),
2085        ],
2086        "Should keep external file with history updates",
2087    );
2088}
2089
2090#[gpui::test]
2091async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
2092    let app_state = init_test(cx);
2093
2094    app_state
2095        .fs
2096        .as_fake()
2097        .insert_tree(
2098            path!("/src"),
2099            json!({
2100                "test": {
2101                    "first.rs": "// First Rust file",
2102                    "second.rs": "// Second Rust file",
2103                    "third.rs": "// Third Rust file",
2104                }
2105            }),
2106        )
2107        .await;
2108
2109    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2110    let (multi_workspace, cx) =
2111        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2112    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2113
2114    // generate some history to select from
2115    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2116    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2117    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2118    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2119
2120    for expected_selected_index in 0..current_history.len() {
2121        cx.dispatch_action(ToggleFileFinder::default());
2122        let picker = active_file_picker(&workspace, cx);
2123        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
2124        assert_eq!(
2125            selected_index, expected_selected_index,
2126            "Should select the next item in the history"
2127        );
2128    }
2129
2130    cx.dispatch_action(ToggleFileFinder::default());
2131    let selected_index = workspace.update(cx, |workspace, cx| {
2132        workspace
2133            .active_modal::<FileFinder>(cx)
2134            .unwrap()
2135            .read(cx)
2136            .picker
2137            .read(cx)
2138            .delegate
2139            .selected_index()
2140    });
2141    assert_eq!(
2142        selected_index, 0,
2143        "Should wrap around the history and start all over"
2144    );
2145}
2146
2147#[gpui::test]
2148async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
2149    let app_state = init_test(cx);
2150
2151    app_state
2152        .fs
2153        .as_fake()
2154        .insert_tree(
2155            path!("/src"),
2156            json!({
2157                "test": {
2158                    "first.rs": "// First Rust file",
2159                    "second.rs": "// Second Rust file",
2160                    "third.rs": "// Third Rust file",
2161                    "fourth.rs": "// Fourth Rust file",
2162                }
2163            }),
2164        )
2165        .await;
2166
2167    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2168    let (multi_workspace, cx) =
2169        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2170    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2171    let worktree_id = cx.read(|cx| {
2172        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2173        assert_eq!(worktrees.len(), 1,);
2174
2175        worktrees[0].read(cx).id()
2176    });
2177
2178    // generate some history to select from
2179    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2180    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2181    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2182    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2183
2184    let finder = open_file_picker(&workspace, cx);
2185    let first_query = "f";
2186    finder
2187        .update_in(cx, |finder, window, cx| {
2188            finder
2189                .delegate
2190                .update_matches(first_query.to_string(), window, cx)
2191        })
2192        .await;
2193    finder.update(cx, |picker, _| {
2194            let matches = collect_search_matches(picker);
2195            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
2196            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
2197            assert_eq!(history_match, &FoundPath::new(
2198                ProjectPath {
2199                    worktree_id,
2200                    path: rel_path("test/first.rs").into(),
2201                },
2202                PathBuf::from(path!("/src/test/first.rs")),
2203            ));
2204            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
2205            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
2206        });
2207
2208    let second_query = "fsdasdsa";
2209    let finder = active_file_picker(&workspace, cx);
2210    finder
2211        .update_in(cx, |finder, window, cx| {
2212            finder
2213                .delegate
2214                .update_matches(second_query.to_string(), window, cx)
2215        })
2216        .await;
2217    finder.update(cx, |picker, _| {
2218        assert!(
2219            collect_search_matches(picker)
2220                .search_paths_only()
2221                .is_empty(),
2222            "No search entries should match {second_query}"
2223        );
2224    });
2225
2226    let first_query_again = first_query;
2227
2228    let finder = active_file_picker(&workspace, cx);
2229    finder
2230        .update_in(cx, |finder, window, cx| {
2231            finder
2232                .delegate
2233                .update_matches(first_query_again.to_string(), window, cx)
2234        })
2235        .await;
2236    finder.update(cx, |picker, _| {
2237            let matches = collect_search_matches(picker);
2238            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");
2239            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
2240            assert_eq!(history_match, &FoundPath::new(
2241                ProjectPath {
2242                    worktree_id,
2243                    path: rel_path("test/first.rs").into(),
2244                },
2245                PathBuf::from(path!("/src/test/first.rs"))
2246            ));
2247            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
2248            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
2249        });
2250}
2251
2252#[gpui::test]
2253async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
2254    let app_state = init_test(cx);
2255
2256    app_state
2257        .fs
2258        .as_fake()
2259        .insert_tree(
2260            path!("/root"),
2261            json!({
2262                "test": {
2263                    "1_qw": "// First file that matches the query",
2264                    "2_second": "// Second file",
2265                    "3_third": "// Third file",
2266                    "4_fourth": "// Fourth file",
2267                    "5_qwqwqw": "// A file with 3 more matches than the first one",
2268                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
2269                    "7_qwqwqw": "// One more, same amount of query matches as above",
2270                }
2271            }),
2272        )
2273        .await;
2274
2275    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2276    let (multi_workspace, cx) =
2277        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2278    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2279    // generate some history to select from
2280    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2281    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2282    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
2283    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2284    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
2285
2286    let finder = open_file_picker(&workspace, cx);
2287    let query = "qw";
2288    finder
2289        .update_in(cx, |finder, window, cx| {
2290            finder
2291                .delegate
2292                .update_matches(query.to_string(), window, cx)
2293        })
2294        .await;
2295    finder.update(cx, |finder, _| {
2296        let search_matches = collect_search_matches(finder);
2297        assert_eq!(
2298            search_matches.history,
2299            vec![
2300                rel_path("test/1_qw").into(),
2301                rel_path("test/6_qwqwqw").into()
2302            ],
2303        );
2304        assert_eq!(
2305            search_matches.search,
2306            vec![
2307                rel_path("test/5_qwqwqw").into(),
2308                rel_path("test/7_qwqwqw").into()
2309            ],
2310        );
2311    });
2312}
2313
2314#[gpui::test]
2315async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
2316    let app_state = init_test(cx);
2317
2318    app_state
2319        .fs
2320        .as_fake()
2321        .insert_tree(
2322            path!("/root"),
2323            json!({
2324                "test": {
2325                    "1_qw": "",
2326                }
2327            }),
2328        )
2329        .await;
2330
2331    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2332    let (multi_workspace, cx) =
2333        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2334    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2335    // Open new buffer
2336    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2337
2338    let picker = open_file_picker(&workspace, cx);
2339    picker.update(cx, |finder, _| {
2340        assert_match_selection(finder, 0, "1_qw");
2341    });
2342}
2343
2344#[gpui::test]
2345async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
2346    cx: &mut TestAppContext,
2347) {
2348    let app_state = init_test(cx);
2349
2350    app_state
2351        .fs
2352        .as_fake()
2353        .insert_tree(
2354            path!("/src"),
2355            json!({
2356                "test": {
2357                    "bar.rs": "// Bar file",
2358                    "lib.rs": "// Lib file",
2359                    "maaa.rs": "// Maaaaaaa",
2360                    "main.rs": "// Main file",
2361                    "moo.rs": "// Moooooo",
2362                }
2363            }),
2364        )
2365        .await;
2366
2367    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2368    let (multi_workspace, cx) =
2369        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2370    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2371
2372    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2373    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2374    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2375
2376    // main.rs is on top, previously used is selected
2377    let picker = open_file_picker(&workspace, cx);
2378    picker.update(cx, |finder, _| {
2379        assert_eq!(finder.delegate.matches.len(), 3);
2380        assert_match_selection(finder, 0, "main.rs");
2381        assert_match_at_position(finder, 1, "lib.rs");
2382        assert_match_at_position(finder, 2, "bar.rs");
2383    });
2384
2385    // all files match, main.rs is still on top, but the second item is selected
2386    picker
2387        .update_in(cx, |finder, window, cx| {
2388            finder
2389                .delegate
2390                .update_matches(".rs".to_string(), window, cx)
2391        })
2392        .await;
2393    picker.update(cx, |finder, _| {
2394        assert_eq!(finder.delegate.matches.len(), 6);
2395        assert_match_at_position(finder, 0, "main.rs");
2396        assert_match_selection(finder, 1, "bar.rs");
2397        assert_match_at_position(finder, 2, "lib.rs");
2398        assert_match_at_position(finder, 3, "moo.rs");
2399        assert_match_at_position(finder, 4, "maaa.rs");
2400        assert_match_at_position(finder, 5, ".rs");
2401    });
2402
2403    // main.rs is not among matches, select top item
2404    picker
2405        .update_in(cx, |finder, window, cx| {
2406            finder.delegate.update_matches("b".to_string(), window, cx)
2407        })
2408        .await;
2409    picker.update(cx, |finder, _| {
2410        assert_eq!(finder.delegate.matches.len(), 3);
2411        assert_match_at_position(finder, 0, "bar.rs");
2412        assert_match_at_position(finder, 1, "lib.rs");
2413        assert_match_at_position(finder, 2, "b");
2414    });
2415
2416    // main.rs is back, put it on top and select next item
2417    picker
2418        .update_in(cx, |finder, window, cx| {
2419            finder.delegate.update_matches("m".to_string(), window, cx)
2420        })
2421        .await;
2422    picker.update(cx, |finder, _| {
2423        assert_eq!(finder.delegate.matches.len(), 4);
2424        assert_match_at_position(finder, 0, "main.rs");
2425        assert_match_selection(finder, 1, "moo.rs");
2426        assert_match_at_position(finder, 2, "maaa.rs");
2427        assert_match_at_position(finder, 3, "m");
2428    });
2429
2430    // get back to the initial state
2431    picker
2432        .update_in(cx, |finder, window, cx| {
2433            finder.delegate.update_matches("".to_string(), window, cx)
2434        })
2435        .await;
2436    picker.update(cx, |finder, _| {
2437        assert_eq!(finder.delegate.matches.len(), 3);
2438        assert_match_selection(finder, 0, "main.rs");
2439        assert_match_at_position(finder, 1, "lib.rs");
2440        assert_match_at_position(finder, 2, "bar.rs");
2441    });
2442}
2443
2444#[gpui::test]
2445async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
2446    let app_state = init_test(cx);
2447
2448    cx.update(|cx| {
2449        let settings = *FileFinderSettings::get_global(cx);
2450
2451        FileFinderSettings::override_global(
2452            FileFinderSettings {
2453                skip_focus_for_active_in_search: false,
2454                ..settings
2455            },
2456            cx,
2457        );
2458    });
2459
2460    app_state
2461        .fs
2462        .as_fake()
2463        .insert_tree(
2464            path!("/src"),
2465            json!({
2466                "test": {
2467                    "bar.rs": "// Bar file",
2468                    "lib.rs": "// Lib file",
2469                    "maaa.rs": "// Maaaaaaa",
2470                    "main.rs": "// Main file",
2471                    "moo.rs": "// Moooooo",
2472                }
2473            }),
2474        )
2475        .await;
2476
2477    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2478    let (multi_workspace, cx) =
2479        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2480    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2481
2482    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2483    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2484    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2485
2486    // main.rs is on top, previously used is selected
2487    let picker = open_file_picker(&workspace, cx);
2488    picker.update(cx, |finder, _| {
2489        assert_eq!(finder.delegate.matches.len(), 3);
2490        assert_match_selection(finder, 0, "main.rs");
2491        assert_match_at_position(finder, 1, "lib.rs");
2492        assert_match_at_position(finder, 2, "bar.rs");
2493    });
2494
2495    // all files match, main.rs is on top, and is selected
2496    picker
2497        .update_in(cx, |finder, window, cx| {
2498            finder
2499                .delegate
2500                .update_matches(".rs".to_string(), window, cx)
2501        })
2502        .await;
2503    picker.update(cx, |finder, _| {
2504        assert_eq!(finder.delegate.matches.len(), 6);
2505        assert_match_selection(finder, 0, "main.rs");
2506        assert_match_at_position(finder, 1, "bar.rs");
2507        assert_match_at_position(finder, 2, "lib.rs");
2508        assert_match_at_position(finder, 3, "moo.rs");
2509        assert_match_at_position(finder, 4, "maaa.rs");
2510        assert_match_at_position(finder, 5, ".rs");
2511    });
2512}
2513
2514#[gpui::test]
2515async fn test_non_separate_history_items(cx: &mut TestAppContext) {
2516    let app_state = init_test(cx);
2517
2518    app_state
2519        .fs
2520        .as_fake()
2521        .insert_tree(
2522            path!("/src"),
2523            json!({
2524                "test": {
2525                    "bar.rs": "// Bar file",
2526                    "lib.rs": "// Lib file",
2527                    "maaa.rs": "// Maaaaaaa",
2528                    "main.rs": "// Main file",
2529                    "moo.rs": "// Moooooo",
2530                }
2531            }),
2532        )
2533        .await;
2534
2535    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2536    let (multi_workspace, cx) =
2537        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2538    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2539
2540    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2541    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2542    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2543
2544    cx.dispatch_action(ToggleFileFinder::default());
2545    let picker = active_file_picker(&workspace, cx);
2546    // main.rs is on top, previously used is selected
2547    picker.update(cx, |finder, _| {
2548        assert_eq!(finder.delegate.matches.len(), 3);
2549        assert_match_selection(finder, 0, "main.rs");
2550        assert_match_at_position(finder, 1, "lib.rs");
2551        assert_match_at_position(finder, 2, "bar.rs");
2552    });
2553
2554    // all files match, main.rs is still on top, but the second item is selected
2555    picker
2556        .update_in(cx, |finder, window, cx| {
2557            finder
2558                .delegate
2559                .update_matches(".rs".to_string(), window, cx)
2560        })
2561        .await;
2562    picker.update(cx, |finder, _| {
2563        assert_eq!(finder.delegate.matches.len(), 6);
2564        assert_match_at_position(finder, 0, "main.rs");
2565        assert_match_selection(finder, 1, "moo.rs");
2566        assert_match_at_position(finder, 2, "bar.rs");
2567        assert_match_at_position(finder, 3, "lib.rs");
2568        assert_match_at_position(finder, 4, "maaa.rs");
2569        assert_match_at_position(finder, 5, ".rs");
2570    });
2571
2572    // main.rs is not among matches, select top item
2573    picker
2574        .update_in(cx, |finder, window, cx| {
2575            finder.delegate.update_matches("b".to_string(), window, cx)
2576        })
2577        .await;
2578    picker.update(cx, |finder, _| {
2579        assert_eq!(finder.delegate.matches.len(), 3);
2580        assert_match_at_position(finder, 0, "bar.rs");
2581        assert_match_at_position(finder, 1, "lib.rs");
2582        assert_match_at_position(finder, 2, "b");
2583    });
2584
2585    // main.rs is back, put it on top and select next item
2586    picker
2587        .update_in(cx, |finder, window, cx| {
2588            finder.delegate.update_matches("m".to_string(), window, cx)
2589        })
2590        .await;
2591    picker.update(cx, |finder, _| {
2592        assert_eq!(finder.delegate.matches.len(), 4);
2593        assert_match_at_position(finder, 0, "main.rs");
2594        assert_match_selection(finder, 1, "moo.rs");
2595        assert_match_at_position(finder, 2, "maaa.rs");
2596        assert_match_at_position(finder, 3, "m");
2597    });
2598
2599    // get back to the initial state
2600    picker
2601        .update_in(cx, |finder, window, cx| {
2602            finder.delegate.update_matches("".to_string(), window, cx)
2603        })
2604        .await;
2605    picker.update(cx, |finder, _| {
2606        assert_eq!(finder.delegate.matches.len(), 3);
2607        assert_match_selection(finder, 0, "main.rs");
2608        assert_match_at_position(finder, 1, "lib.rs");
2609        assert_match_at_position(finder, 2, "bar.rs");
2610    });
2611}
2612
2613#[gpui::test]
2614async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
2615    let app_state = init_test(cx);
2616
2617    app_state
2618        .fs
2619        .as_fake()
2620        .insert_tree(
2621            path!("/test"),
2622            json!({
2623                "test": {
2624                    "1.txt": "// One",
2625                    "2.txt": "// Two",
2626                    "3.txt": "// Three",
2627                }
2628            }),
2629        )
2630        .await;
2631
2632    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2633    let (multi_workspace, cx) =
2634        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2635    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2636
2637    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2638    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2639    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2640
2641    let picker = open_file_picker(&workspace, cx);
2642    picker.update(cx, |finder, _| {
2643        assert_eq!(finder.delegate.matches.len(), 3);
2644        assert_match_selection(finder, 0, "3.txt");
2645        assert_match_at_position(finder, 1, "2.txt");
2646        assert_match_at_position(finder, 2, "1.txt");
2647    });
2648
2649    cx.dispatch_action(SelectNext);
2650    cx.dispatch_action(Confirm); // Open 2.txt
2651
2652    let picker = open_file_picker(&workspace, cx);
2653    picker.update(cx, |finder, _| {
2654        assert_eq!(finder.delegate.matches.len(), 3);
2655        assert_match_selection(finder, 0, "2.txt");
2656        assert_match_at_position(finder, 1, "3.txt");
2657        assert_match_at_position(finder, 2, "1.txt");
2658    });
2659
2660    cx.dispatch_action(SelectNext);
2661    cx.dispatch_action(SelectNext);
2662    cx.dispatch_action(Confirm); // Open 1.txt
2663
2664    let picker = open_file_picker(&workspace, cx);
2665    picker.update(cx, |finder, _| {
2666        assert_eq!(finder.delegate.matches.len(), 3);
2667        assert_match_selection(finder, 0, "1.txt");
2668        assert_match_at_position(finder, 1, "2.txt");
2669        assert_match_at_position(finder, 2, "3.txt");
2670    });
2671}
2672
2673#[gpui::test]
2674async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2675    let app_state = init_test(cx);
2676
2677    app_state
2678        .fs
2679        .as_fake()
2680        .insert_tree(
2681            path!("/test"),
2682            json!({
2683                "test": {
2684                    "1.txt": "// One",
2685                    "2.txt": "// Two",
2686                    "3.txt": "// Three",
2687                }
2688            }),
2689        )
2690        .await;
2691
2692    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2693    let (multi_workspace, cx) =
2694        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2695    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2696
2697    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2698    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2699    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2700
2701    let picker = open_file_picker(&workspace, cx);
2702    picker.update(cx, |finder, _| {
2703        assert_eq!(finder.delegate.matches.len(), 3);
2704        assert_match_selection(finder, 0, "3.txt");
2705        assert_match_at_position(finder, 1, "2.txt");
2706        assert_match_at_position(finder, 2, "1.txt");
2707    });
2708
2709    cx.dispatch_action(SelectNext);
2710
2711    // Add more files to the worktree to trigger update matches
2712    for i in 0..5 {
2713        let filename = if cfg!(windows) {
2714            format!("C:/test/{}.txt", 4 + i)
2715        } else {
2716            format!("/test/{}.txt", 4 + i)
2717        };
2718        app_state
2719            .fs
2720            .create_file(Path::new(&filename), Default::default())
2721            .await
2722            .expect("unable to create file");
2723    }
2724
2725    cx.executor().advance_clock(FS_WATCH_LATENCY);
2726
2727    picker.update(cx, |finder, _| {
2728        assert_eq!(finder.delegate.matches.len(), 3);
2729        assert_match_at_position(finder, 0, "3.txt");
2730        assert_match_selection(finder, 1, "2.txt");
2731        assert_match_at_position(finder, 2, "1.txt");
2732    });
2733}
2734
2735#[gpui::test]
2736async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2737    let app_state = init_test(cx);
2738
2739    app_state
2740        .fs
2741        .as_fake()
2742        .insert_tree(
2743            path!("/src"),
2744            json!({
2745                "collab_ui": {
2746                    "first.rs": "// First Rust file",
2747                    "second.rs": "// Second Rust file",
2748                    "third.rs": "// Third Rust file",
2749                    "collab_ui.rs": "// Fourth Rust file",
2750                }
2751            }),
2752        )
2753        .await;
2754
2755    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2756    let (multi_workspace, cx) =
2757        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2758    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2759    // generate some history to select from
2760    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2761    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2762    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2763    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2764
2765    let finder = open_file_picker(&workspace, cx);
2766    let query = "collab_ui";
2767    cx.simulate_input(query);
2768    finder.update(cx, |picker, _| {
2769            let search_entries = collect_search_matches(picker).search_paths_only();
2770            assert_eq!(
2771                search_entries,
2772                vec![
2773                    rel_path("collab_ui/collab_ui.rs").into(),
2774                    rel_path("collab_ui/first.rs").into(),
2775                    rel_path("collab_ui/third.rs").into(),
2776                    rel_path("collab_ui/second.rs").into(),
2777                ],
2778                "Despite all search results having the same directory name, the most matching one should be on top"
2779            );
2780        });
2781}
2782
2783#[gpui::test]
2784async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2785    let app_state = init_test(cx);
2786
2787    app_state
2788        .fs
2789        .as_fake()
2790        .insert_tree(
2791            path!("/src"),
2792            json!({
2793                "test": {
2794                    "first.rs": "// First Rust file",
2795                    "nonexistent.rs": "// Second Rust file",
2796                    "third.rs": "// Third Rust file",
2797                }
2798            }),
2799        )
2800        .await;
2801
2802    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2803    let (multi_workspace, cx) =
2804        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); // generate some history to select from
2805    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2806    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2807    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2808    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2809    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2810    app_state
2811        .fs
2812        .remove_file(
2813            Path::new(path!("/src/test/nonexistent.rs")),
2814            RemoveOptions::default(),
2815        )
2816        .await
2817        .unwrap();
2818    cx.run_until_parked();
2819
2820    let picker = open_file_picker(&workspace, cx);
2821    cx.simulate_input("rs");
2822
2823    picker.update(cx, |picker, _| {
2824        assert_eq!(
2825            collect_search_matches(picker).history,
2826            vec![
2827                rel_path("test/first.rs").into(),
2828                rel_path("test/third.rs").into()
2829            ],
2830            "Should have all opened files in the history, except the ones that do not exist on disk"
2831        );
2832    });
2833}
2834
2835#[gpui::test]
2836async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2837    let app_state = init_test(cx);
2838
2839    app_state
2840        .fs
2841        .as_fake()
2842        .insert_tree(
2843            "/src",
2844            json!({
2845                "lib.rs": "// Lib file",
2846                "main.rs": "// Bar file",
2847                "read.me": "// Readme file",
2848            }),
2849        )
2850        .await;
2851
2852    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2853    let (multi_workspace, cx) =
2854        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2855    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2856
2857    // Initial state
2858    let picker = open_file_picker(&workspace, cx);
2859    cx.simulate_input("rs");
2860    picker.update(cx, |finder, _| {
2861        assert_eq!(finder.delegate.matches.len(), 3);
2862        assert_match_at_position(finder, 0, "lib.rs");
2863        assert_match_at_position(finder, 1, "main.rs");
2864        assert_match_at_position(finder, 2, "rs");
2865    });
2866    // Delete main.rs
2867    app_state
2868        .fs
2869        .remove_file("/src/main.rs".as_ref(), Default::default())
2870        .await
2871        .expect("unable to remove file");
2872    cx.executor().advance_clock(FS_WATCH_LATENCY);
2873
2874    // main.rs is in not among search results anymore
2875    picker.update(cx, |finder, _| {
2876        assert_eq!(finder.delegate.matches.len(), 2);
2877        assert_match_at_position(finder, 0, "lib.rs");
2878        assert_match_at_position(finder, 1, "rs");
2879    });
2880
2881    // Create util.rs
2882    app_state
2883        .fs
2884        .create_file("/src/util.rs".as_ref(), Default::default())
2885        .await
2886        .expect("unable to create file");
2887    cx.executor().advance_clock(FS_WATCH_LATENCY);
2888
2889    // util.rs is among search results
2890    picker.update(cx, |finder, _| {
2891        assert_eq!(finder.delegate.matches.len(), 3);
2892        assert_match_at_position(finder, 0, "lib.rs");
2893        assert_match_at_position(finder, 1, "util.rs");
2894        assert_match_at_position(finder, 2, "rs");
2895    });
2896}
2897
2898#[gpui::test]
2899async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
2900    let app_state = init_test(cx);
2901
2902    app_state
2903        .fs
2904        .as_fake()
2905        .insert_tree(
2906            "/src",
2907            json!({
2908                "lib.rs": "// Lib file",
2909                "main.rs": "// Bar file",
2910                "read.me": "// Readme file",
2911            }),
2912        )
2913        .await;
2914    app_state
2915        .fs
2916        .as_fake()
2917        .insert_tree(
2918            "/test",
2919            json!({
2920                "new.rs": "// New file",
2921            }),
2922        )
2923        .await;
2924
2925    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2926    let window = cx.add_window({
2927        let project = project.clone();
2928        |window, cx| MultiWorkspace::test_new(project, window, cx)
2929    });
2930    let cx = VisualTestContext::from_window(*window, cx).into_mut();
2931    let workspace = window
2932        .read_with(cx, |mw, _| mw.workspace().clone())
2933        .unwrap();
2934
2935    cx.update(|_, cx| {
2936        open_paths(
2937            &[PathBuf::from(path!("/test/new.rs"))],
2938            app_state,
2939            workspace::OpenOptions::default(),
2940            cx,
2941        )
2942    })
2943    .await
2944    .unwrap();
2945    assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
2946
2947    let initial_history = open_close_queried_buffer("new", 1, "new.rs", &workspace, cx).await;
2948    assert_eq!(
2949        initial_history.first().unwrap().absolute,
2950        PathBuf::from(path!("/test/new.rs")),
2951        "Should show 1st opened item in the history when opening the 2nd item"
2952    );
2953
2954    let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2955    assert_eq!(
2956        history_after_first.first().unwrap().absolute,
2957        PathBuf::from(path!("/test/new.rs")),
2958        "Should show 1st opened item in the history when opening the 2nd item"
2959    );
2960}
2961
2962#[gpui::test]
2963async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2964    cx: &mut gpui::TestAppContext,
2965) {
2966    let app_state = init_test(cx);
2967
2968    app_state
2969        .fs
2970        .as_fake()
2971        .insert_tree(
2972            "/test",
2973            json!({
2974                "project_1": {
2975                    "bar.rs": "// Bar file",
2976                    "lib.rs": "// Lib file",
2977                },
2978                "project_2": {
2979                    "Cargo.toml": "// Cargo file",
2980                    "main.rs": "// Main file",
2981                }
2982            }),
2983        )
2984        .await;
2985
2986    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2987    let (multi_workspace, cx) =
2988        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2989    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2990    let worktree_1_id = project.update(cx, |project, cx| {
2991        let worktree = project.worktrees(cx).last().expect("worktree not found");
2992        worktree.read(cx).id()
2993    });
2994
2995    // Initial state
2996    let picker = open_file_picker(&workspace, cx);
2997    cx.simulate_input("rs");
2998    picker.update(cx, |finder, _| {
2999        assert_eq!(finder.delegate.matches.len(), 3);
3000        assert_match_at_position(finder, 0, "bar.rs");
3001        assert_match_at_position(finder, 1, "lib.rs");
3002        assert_match_at_position(finder, 2, "rs");
3003    });
3004
3005    // Add new worktree
3006    project
3007        .update(cx, |project, cx| {
3008            project
3009                .find_or_create_worktree("/test/project_2", true, cx)
3010                .into_future()
3011        })
3012        .await
3013        .expect("unable to create workdir");
3014    cx.executor().advance_clock(FS_WATCH_LATENCY);
3015
3016    // main.rs is among search results
3017    picker.update(cx, |finder, _| {
3018        assert_eq!(finder.delegate.matches.len(), 4);
3019        assert_match_at_position(finder, 0, "bar.rs");
3020        assert_match_at_position(finder, 1, "lib.rs");
3021        assert_match_at_position(finder, 2, "main.rs");
3022        assert_match_at_position(finder, 3, "rs");
3023    });
3024
3025    // Remove the first worktree
3026    project.update(cx, |project, cx| {
3027        project.remove_worktree(worktree_1_id, cx);
3028    });
3029    cx.executor().advance_clock(FS_WATCH_LATENCY);
3030
3031    // Files from the first worktree are not in the search results anymore
3032    picker.update(cx, |finder, _| {
3033        assert_eq!(finder.delegate.matches.len(), 2);
3034        assert_match_at_position(finder, 0, "main.rs");
3035        assert_match_at_position(finder, 1, "rs");
3036    });
3037}
3038
3039#[gpui::test]
3040async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
3041    cx: &mut TestAppContext,
3042) {
3043    let app_state = init_test(cx);
3044    app_state
3045        .fs
3046        .as_fake()
3047        .insert_tree(
3048            path!("/repo1"),
3049            json!({
3050                "package.json": r#"{"name": "repo1"}"#,
3051                "src": {
3052                    "index.js": "// Repo 1 index",
3053                }
3054            }),
3055        )
3056        .await;
3057
3058    app_state
3059        .fs
3060        .as_fake()
3061        .insert_tree(
3062            path!("/repo2"),
3063            json!({
3064                "package.json": r#"{"name": "repo2"}"#,
3065                "src": {
3066                    "index.js": "// Repo 2 index",
3067                }
3068            }),
3069        )
3070        .await;
3071
3072    let project = Project::test(
3073        app_state.fs.clone(),
3074        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
3075        cx,
3076    )
3077    .await;
3078
3079    let (multi_workspace, cx) =
3080        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3081    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3082    let (worktree_id1, worktree_id2) = cx.read(|cx| {
3083        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
3084        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
3085    });
3086
3087    workspace
3088        .update_in(cx, |workspace, window, cx| {
3089            workspace.open_path(
3090                ProjectPath {
3091                    worktree_id: worktree_id1,
3092                    path: rel_path("package.json").into(),
3093                },
3094                None,
3095                true,
3096                window,
3097                cx,
3098            )
3099        })
3100        .await
3101        .unwrap();
3102
3103    cx.dispatch_action(workspace::CloseActiveItem {
3104        save_intent: None,
3105        close_pinned: false,
3106    });
3107    workspace
3108        .update_in(cx, |workspace, window, cx| {
3109            workspace.open_path(
3110                ProjectPath {
3111                    worktree_id: worktree_id2,
3112                    path: rel_path("package.json").into(),
3113                },
3114                None,
3115                true,
3116                window,
3117                cx,
3118            )
3119        })
3120        .await
3121        .unwrap();
3122
3123    cx.dispatch_action(workspace::CloseActiveItem {
3124        save_intent: None,
3125        close_pinned: false,
3126    });
3127
3128    let picker = open_file_picker(&workspace, cx);
3129    cx.simulate_input("package.json");
3130
3131    picker.update(cx, |finder, _| {
3132        let matches = &finder.delegate.matches.matches;
3133
3134        assert_eq!(
3135            matches.len(),
3136            2,
3137            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
3138            matches.len(),
3139            matches
3140        );
3141
3142        assert_matches!(matches[0], Match::History { .. });
3143
3144        let search_matches = collect_search_matches(finder);
3145        assert_eq!(
3146            search_matches.history.len(),
3147            2,
3148            "Should have exactly 2 history match"
3149        );
3150        assert_eq!(
3151            search_matches.search.len(),
3152            0,
3153            "Should have exactly 0 search match (because we already opened the 2 package.json)"
3154        );
3155
3156        if let Match::History { path, panel_match } = &matches[0] {
3157            assert_eq!(path.project.worktree_id, worktree_id2);
3158            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
3159            let panel_match = panel_match.as_ref().unwrap();
3160            assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
3161            assert_eq!(panel_match.0.path, rel_path("package.json").into());
3162            assert_eq!(
3163                panel_match.0.positions,
3164                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
3165            );
3166        }
3167
3168        if let Match::History { path, panel_match } = &matches[1] {
3169            assert_eq!(path.project.worktree_id, worktree_id1);
3170            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
3171            let panel_match = panel_match.as_ref().unwrap();
3172            assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
3173            assert_eq!(panel_match.0.path, rel_path("package.json").into());
3174            assert_eq!(
3175                panel_match.0.positions,
3176                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
3177            );
3178        }
3179    });
3180}
3181
3182#[gpui::test]
3183async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
3184    let app_state = init_test(cx);
3185
3186    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
3187
3188    app_state
3189        .fs
3190        .create_dir("/src/even".as_ref())
3191        .await
3192        .expect("unable to create dir");
3193
3194    let initial_files_num = 5;
3195    for i in 0..initial_files_num {
3196        let filename = format!("/src/even/file_{}.txt", 10 + i);
3197        app_state
3198            .fs
3199            .create_file(Path::new(&filename), Default::default())
3200            .await
3201            .expect("unable to create file");
3202    }
3203
3204    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3205    let (multi_workspace, cx) =
3206        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3207    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3208
3209    // Initial state
3210    let picker = open_file_picker(&workspace, cx);
3211    cx.simulate_input("file");
3212    let selected_index = 3;
3213    // Checking only the filename, not the whole path
3214    let selected_file = format!("file_{}.txt", 10 + selected_index);
3215    // Select even/file_13.txt
3216    for _ in 0..selected_index {
3217        cx.dispatch_action(SelectNext);
3218    }
3219
3220    picker.update(cx, |finder, _| {
3221        assert_match_selection(finder, selected_index, &selected_file)
3222    });
3223
3224    // Add more matches to the search results
3225    let files_to_add = 10;
3226    for i in 0..files_to_add {
3227        let filename = format!("/src/file_{}.txt", 20 + i);
3228        app_state
3229            .fs
3230            .create_file(Path::new(&filename), Default::default())
3231            .await
3232            .expect("unable to create file");
3233        // Wait for each file system event to be fully processed before adding the next
3234        cx.executor().advance_clock(FS_WATCH_LATENCY);
3235        cx.run_until_parked();
3236    }
3237
3238    // file_13.txt is still selected
3239    picker.update(cx, |finder, _| {
3240        let expected_selected_index = selected_index + files_to_add;
3241        assert_match_selection(finder, expected_selected_index, &selected_file);
3242    });
3243}
3244
3245#[gpui::test]
3246async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
3247    cx: &mut gpui::TestAppContext,
3248) {
3249    let app_state = init_test(cx);
3250
3251    app_state
3252        .fs
3253        .as_fake()
3254        .insert_tree(
3255            "/src",
3256            json!({
3257                "file_1.txt": "// file_1",
3258                "file_2.txt": "// file_2",
3259                "file_3.txt": "// file_3",
3260            }),
3261        )
3262        .await;
3263
3264    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3265    let (multi_workspace, cx) =
3266        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3267    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3268
3269    // Initial state
3270    let picker = open_file_picker(&workspace, cx);
3271    cx.simulate_input("file");
3272    // Select even/file_2.txt
3273    cx.dispatch_action(SelectNext);
3274
3275    // Remove the selected entry
3276    app_state
3277        .fs
3278        .remove_file("/src/file_2.txt".as_ref(), Default::default())
3279        .await
3280        .expect("unable to remove file");
3281    cx.executor().advance_clock(FS_WATCH_LATENCY);
3282
3283    // file_1.txt is now selected
3284    picker.update(cx, |finder, _| {
3285        assert_match_selection(finder, 0, "file_1.txt");
3286    });
3287}
3288
3289#[gpui::test]
3290async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
3291    let app_state = init_test(cx);
3292
3293    app_state
3294        .fs
3295        .as_fake()
3296        .insert_tree(
3297            path!("/test"),
3298            json!({
3299                "1.txt": "// One",
3300            }),
3301        )
3302        .await;
3303
3304    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3305    let (multi_workspace, cx) =
3306        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3307    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3308
3309    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3310
3311    cx.simulate_modifiers_change(Modifiers::secondary_key());
3312    open_file_picker(&workspace, cx);
3313
3314    cx.simulate_modifiers_change(Modifiers::none());
3315    active_file_picker(&workspace, cx);
3316}
3317
3318#[gpui::test]
3319async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
3320    let app_state = init_test(cx);
3321
3322    app_state
3323        .fs
3324        .as_fake()
3325        .insert_tree(
3326            path!("/test"),
3327            json!({
3328                "1.txt": "// One",
3329                "2.txt": "// Two",
3330            }),
3331        )
3332        .await;
3333
3334    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3335    let (multi_workspace, cx) =
3336        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3337    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3338
3339    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3340    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3341
3342    cx.simulate_modifiers_change(Modifiers::secondary_key());
3343    let picker = open_file_picker(&workspace, cx);
3344    picker.update(cx, |finder, _| {
3345        assert_eq!(finder.delegate.matches.len(), 2);
3346        assert_match_selection(finder, 0, "2.txt");
3347        assert_match_at_position(finder, 1, "1.txt");
3348    });
3349
3350    cx.dispatch_action(SelectNext);
3351    cx.simulate_modifiers_change(Modifiers::none());
3352    cx.read(|cx| {
3353        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3354        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
3355    });
3356}
3357
3358#[gpui::test]
3359async fn test_switches_between_release_norelease_modes_on_forward_nav(
3360    cx: &mut gpui::TestAppContext,
3361) {
3362    let app_state = init_test(cx);
3363
3364    app_state
3365        .fs
3366        .as_fake()
3367        .insert_tree(
3368            path!("/test"),
3369            json!({
3370                "1.txt": "// One",
3371                "2.txt": "// Two",
3372            }),
3373        )
3374        .await;
3375
3376    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3377    let (multi_workspace, cx) =
3378        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3379    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3380
3381    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3382    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3383
3384    // Open with a shortcut
3385    cx.simulate_modifiers_change(Modifiers::secondary_key());
3386    let picker = open_file_picker(&workspace, cx);
3387    picker.update(cx, |finder, _| {
3388        assert_eq!(finder.delegate.matches.len(), 2);
3389        assert_match_selection(finder, 0, "2.txt");
3390        assert_match_at_position(finder, 1, "1.txt");
3391    });
3392
3393    // Switch to navigating with other shortcuts
3394    // Don't open file on modifiers release
3395    cx.simulate_modifiers_change(Modifiers::control());
3396    cx.dispatch_action(SelectNext);
3397    cx.simulate_modifiers_change(Modifiers::none());
3398    picker.update(cx, |finder, _| {
3399        assert_eq!(finder.delegate.matches.len(), 2);
3400        assert_match_at_position(finder, 0, "2.txt");
3401        assert_match_selection(finder, 1, "1.txt");
3402    });
3403
3404    // Back to navigation with initial shortcut
3405    // Open file on modifiers release
3406    cx.simulate_modifiers_change(Modifiers::secondary_key());
3407    cx.dispatch_action(ToggleFileFinder::default());
3408    cx.simulate_modifiers_change(Modifiers::none());
3409    cx.read(|cx| {
3410        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3411        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
3412    });
3413}
3414
3415#[gpui::test]
3416async fn test_switches_between_release_norelease_modes_on_backward_nav(
3417    cx: &mut gpui::TestAppContext,
3418) {
3419    let app_state = init_test(cx);
3420
3421    app_state
3422        .fs
3423        .as_fake()
3424        .insert_tree(
3425            path!("/test"),
3426            json!({
3427                "1.txt": "// One",
3428                "2.txt": "// Two",
3429                "3.txt": "// Three"
3430            }),
3431        )
3432        .await;
3433
3434    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3435    let (multi_workspace, cx) =
3436        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3437    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3438
3439    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3440    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3441    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
3442
3443    // Open with a shortcut
3444    cx.simulate_modifiers_change(Modifiers::secondary_key());
3445    let picker = open_file_picker(&workspace, cx);
3446    picker.update(cx, |finder, _| {
3447        assert_eq!(finder.delegate.matches.len(), 3);
3448        assert_match_selection(finder, 0, "3.txt");
3449        assert_match_at_position(finder, 1, "2.txt");
3450        assert_match_at_position(finder, 2, "1.txt");
3451    });
3452
3453    // Switch to navigating with other shortcuts
3454    // Don't open file on modifiers release
3455    cx.simulate_modifiers_change(Modifiers::control());
3456    cx.dispatch_action(menu::SelectPrevious);
3457    cx.simulate_modifiers_change(Modifiers::none());
3458    picker.update(cx, |finder, _| {
3459        assert_eq!(finder.delegate.matches.len(), 3);
3460        assert_match_at_position(finder, 0, "3.txt");
3461        assert_match_at_position(finder, 1, "2.txt");
3462        assert_match_selection(finder, 2, "1.txt");
3463    });
3464
3465    // Back to navigation with initial shortcut
3466    // Open file on modifiers release
3467    cx.simulate_modifiers_change(Modifiers::secondary_key());
3468    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
3469    cx.simulate_modifiers_change(Modifiers::none());
3470    cx.read(|cx| {
3471        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3472        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
3473    });
3474}
3475
3476#[gpui::test]
3477async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
3478    let app_state = init_test(cx);
3479
3480    app_state
3481        .fs
3482        .as_fake()
3483        .insert_tree(
3484            path!("/test"),
3485            json!({
3486                "1.txt": "// One",
3487            }),
3488        )
3489        .await;
3490
3491    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3492    let (multi_workspace, cx) =
3493        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3494    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3495
3496    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3497
3498    cx.simulate_modifiers_change(Modifiers::secondary_key());
3499    open_file_picker(&workspace, cx);
3500
3501    cx.simulate_modifiers_change(Modifiers::command_shift());
3502    active_file_picker(&workspace, cx);
3503}
3504
3505#[gpui::test]
3506async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
3507    let app_state = init_test(cx);
3508    app_state
3509        .fs
3510        .as_fake()
3511        .insert_tree(
3512            "/test",
3513            json!({
3514                "00.txt": "",
3515                "01.txt": "",
3516                "02.txt": "",
3517                "03.txt": "",
3518                "04.txt": "",
3519                "05.txt": "",
3520            }),
3521        )
3522        .await;
3523
3524    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
3525    let (multi_workspace, cx) =
3526        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3527    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3528
3529    cx.dispatch_action(ToggleFileFinder::default());
3530    let picker = active_file_picker(&workspace, cx);
3531
3532    picker.update_in(cx, |picker, window, cx| {
3533        picker.update_matches(".txt".to_string(), window, cx)
3534    });
3535
3536    cx.run_until_parked();
3537
3538    picker.update(cx, |picker, _| {
3539        assert_eq!(picker.delegate.matches.len(), 7);
3540        assert_eq!(picker.delegate.selected_index, 0);
3541    });
3542
3543    // When toggling repeatedly, the picker scrolls to reveal the selected item.
3544    cx.dispatch_action(ToggleFileFinder::default());
3545    cx.dispatch_action(ToggleFileFinder::default());
3546    cx.dispatch_action(ToggleFileFinder::default());
3547
3548    cx.run_until_parked();
3549
3550    picker.update(cx, |picker, _| {
3551        assert_eq!(picker.delegate.matches.len(), 7);
3552        assert_eq!(picker.delegate.selected_index, 3);
3553    });
3554}
3555
3556async fn open_close_queried_buffer(
3557    input: &str,
3558    expected_matches: usize,
3559    expected_editor_title: &str,
3560    workspace: &Entity<Workspace>,
3561    cx: &mut gpui::VisualTestContext,
3562) -> Vec<FoundPath> {
3563    let history_items = open_queried_buffer(
3564        input,
3565        expected_matches,
3566        expected_editor_title,
3567        workspace,
3568        cx,
3569    )
3570    .await;
3571
3572    cx.dispatch_action(workspace::CloseActiveItem {
3573        save_intent: None,
3574        close_pinned: false,
3575    });
3576
3577    history_items
3578}
3579
3580async fn open_queried_buffer(
3581    input: &str,
3582    expected_matches: usize,
3583    expected_editor_title: &str,
3584    workspace: &Entity<Workspace>,
3585    cx: &mut gpui::VisualTestContext,
3586) -> Vec<FoundPath> {
3587    let picker = open_file_picker(workspace, cx);
3588    cx.simulate_input(input);
3589
3590    let history_items = picker.update(cx, |finder, _| {
3591        assert_eq!(
3592            finder.delegate.matches.len(),
3593            expected_matches + 1, // +1 from CreateNew option
3594            "Unexpected number of matches found for query `{input}`, matches: {:?}",
3595            finder.delegate.matches
3596        );
3597        finder.delegate.history_items.clone()
3598    });
3599
3600    cx.dispatch_action(Confirm);
3601
3602    cx.read(|cx| {
3603        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3604        let active_editor_title = active_editor.read(cx).title(cx);
3605        assert_eq!(
3606            expected_editor_title, active_editor_title,
3607            "Unexpected editor title for query `{input}`"
3608        );
3609    });
3610
3611    history_items
3612}
3613
3614fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3615    cx.update(|cx| {
3616        let state = AppState::test(cx);
3617        theme::init(theme::LoadThemes::JustBase, cx);
3618        super::init(cx);
3619        editor::init(cx);
3620        state
3621    })
3622}
3623
3624fn test_path_position(test_str: &str) -> FileSearchQuery {
3625    let path_position = PathWithPosition::parse_str(test_str);
3626
3627    FileSearchQuery {
3628        raw_query: test_str.to_owned(),
3629        file_query_end: if path_position.path.to_str().unwrap() == test_str {
3630            None
3631        } else {
3632            Some(path_position.path.to_str().unwrap().len())
3633        },
3634        path_position,
3635    }
3636}
3637
3638fn build_find_picker(
3639    project: Entity<Project>,
3640    cx: &mut TestAppContext,
3641) -> (
3642    Entity<Picker<FileFinderDelegate>>,
3643    Entity<Workspace>,
3644    &mut VisualTestContext,
3645) {
3646    let (multi_workspace, cx) =
3647        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3648    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3649    let picker = open_file_picker(&workspace, cx);
3650    (picker, workspace, cx)
3651}
3652
3653#[track_caller]
3654fn open_file_picker(
3655    workspace: &Entity<Workspace>,
3656    cx: &mut VisualTestContext,
3657) -> Entity<Picker<FileFinderDelegate>> {
3658    cx.dispatch_action(ToggleFileFinder {
3659        separate_history: true,
3660    });
3661    active_file_picker(workspace, cx)
3662}
3663
3664#[track_caller]
3665fn active_file_picker(
3666    workspace: &Entity<Workspace>,
3667    cx: &mut VisualTestContext,
3668) -> Entity<Picker<FileFinderDelegate>> {
3669    workspace.update(cx, |workspace, cx| {
3670        workspace
3671            .active_modal::<FileFinder>(cx)
3672            .expect("file finder is not open")
3673            .read(cx)
3674            .picker
3675            .clone()
3676    })
3677}
3678
3679#[derive(Debug, Default)]
3680struct SearchEntries {
3681    history: Vec<Arc<RelPath>>,
3682    history_found_paths: Vec<FoundPath>,
3683    search: Vec<Arc<RelPath>>,
3684    search_matches: Vec<PathMatch>,
3685}
3686
3687impl SearchEntries {
3688    #[track_caller]
3689    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
3690        assert!(
3691            self.history.is_empty(),
3692            "Should have no history matches, but got: {:?}",
3693            self.history
3694        );
3695        self.search
3696    }
3697
3698    #[track_caller]
3699    fn search_matches_only(self) -> Vec<PathMatch> {
3700        assert!(
3701            self.history.is_empty(),
3702            "Should have no history matches, but got: {:?}",
3703            self.history
3704        );
3705        self.search_matches
3706    }
3707}
3708
3709fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
3710    let mut search_entries = SearchEntries::default();
3711    for m in &picker.delegate.matches.matches {
3712        match &m {
3713            Match::History {
3714                path: history_path,
3715                panel_match: path_match,
3716            } => {
3717                if let Some(path_match) = path_match.as_ref() {
3718                    search_entries
3719                        .history
3720                        .push(path_match.0.path_prefix.join(&path_match.0.path));
3721                } else {
3722                    // This occurs when the query is empty and we show history matches
3723                    // that are outside the project.
3724                    panic!("currently not exercised in tests");
3725                }
3726                search_entries
3727                    .history_found_paths
3728                    .push(history_path.clone());
3729            }
3730            Match::Search(path_match) => {
3731                search_entries
3732                    .search
3733                    .push(path_match.0.path_prefix.join(&path_match.0.path));
3734                search_entries.search_matches.push(path_match.0.clone());
3735            }
3736            Match::CreateNew(_) => {}
3737        }
3738    }
3739    search_entries
3740}
3741
3742#[track_caller]
3743fn assert_match_selection(
3744    finder: &Picker<FileFinderDelegate>,
3745    expected_selection_index: usize,
3746    expected_file_name: &str,
3747) {
3748    assert_eq!(
3749        finder.delegate.selected_index(),
3750        expected_selection_index,
3751        "Match is not selected"
3752    );
3753    assert_match_at_position(finder, expected_selection_index, expected_file_name);
3754}
3755
3756#[track_caller]
3757fn assert_match_at_position(
3758    finder: &Picker<FileFinderDelegate>,
3759    match_index: usize,
3760    expected_file_name: &str,
3761) {
3762    let match_item = finder
3763        .delegate
3764        .matches
3765        .get(match_index)
3766        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
3767    let match_file_name = match &match_item {
3768        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
3769        Match::Search(path_match) => path_match.0.path.file_name(),
3770        Match::CreateNew(project_path) => project_path.path.file_name(),
3771    }
3772    .unwrap();
3773    assert_eq!(match_file_name, expected_file_name);
3774}
3775
3776#[gpui::test]
3777async fn test_filename_precedence(cx: &mut TestAppContext) {
3778    let app_state = init_test(cx);
3779
3780    app_state
3781        .fs
3782        .as_fake()
3783        .insert_tree(
3784            path!("/src"),
3785            json!({
3786                "layout": {
3787                    "app.css": "",
3788                    "app.d.ts": "",
3789                    "app.html": "",
3790                    "+page.svelte": "",
3791                },
3792                "routes": {
3793                    "+layout.svelte": "",
3794                }
3795            }),
3796        )
3797        .await;
3798
3799    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3800    let (picker, _, cx) = build_find_picker(project, cx);
3801
3802    cx.simulate_input("layout");
3803
3804    picker.update(cx, |finder, _| {
3805        let search_matches = collect_search_matches(finder).search_paths_only();
3806
3807        assert_eq!(
3808            search_matches,
3809            vec![
3810                rel_path("routes/+layout.svelte").into(),
3811                rel_path("layout/app.css").into(),
3812                rel_path("layout/app.d.ts").into(),
3813                rel_path("layout/app.html").into(),
3814                rel_path("layout/+page.svelte").into(),
3815            ],
3816            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
3817        );
3818    });
3819}
3820
3821#[gpui::test]
3822async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
3823    let app_state = init_test(cx);
3824    app_state
3825        .fs
3826        .as_fake()
3827        .insert_tree(
3828            path!("/root"),
3829            json!({
3830                "a": {
3831                    "file1.txt": "",
3832                    "b": {
3833                        "file2.txt": "",
3834                    },
3835                }
3836            }),
3837        )
3838        .await;
3839
3840    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3841
3842    let (picker, workspace, cx) = build_find_picker(project, cx);
3843
3844    let matching_abs_path = "/file1.txt".to_string();
3845    picker
3846        .update_in(cx, |picker, window, cx| {
3847            picker
3848                .delegate
3849                .update_matches(matching_abs_path, window, cx)
3850        })
3851        .await;
3852    picker.update(cx, |picker, _| {
3853        assert_eq!(
3854            collect_search_matches(picker).search_paths_only(),
3855            vec![rel_path("a/file1.txt").into()],
3856            "Relative path starting with slash should match"
3857        )
3858    });
3859    cx.dispatch_action(SelectNext);
3860    cx.dispatch_action(Confirm);
3861    cx.read(|cx| {
3862        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3863        assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
3864    });
3865}
3866
3867#[gpui::test]
3868async fn test_clear_navigation_history(cx: &mut TestAppContext) {
3869    let app_state = init_test(cx);
3870    app_state
3871        .fs
3872        .as_fake()
3873        .insert_tree(
3874            path!("/src"),
3875            json!({
3876                "test": {
3877                    "first.rs": "// First file",
3878                    "second.rs": "// Second file",
3879                    "third.rs": "// Third file",
3880                }
3881            }),
3882        )
3883        .await;
3884
3885    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3886    let (multi_workspace, cx) =
3887        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3888    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3889
3890    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
3891
3892    // Open some files to generate navigation history
3893    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
3894    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3895    let history_before_clear =
3896        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
3897
3898    assert_eq!(
3899        history_before_clear.len(),
3900        2,
3901        "Should have history items before clearing"
3902    );
3903
3904    // Verify that file finder shows history items
3905    let picker = open_file_picker(&workspace, cx);
3906    cx.simulate_input("fir");
3907    picker.update(cx, |finder, _| {
3908        let matches = collect_search_matches(finder);
3909        assert!(
3910            !matches.history.is_empty(),
3911            "File finder should show history items before clearing"
3912        );
3913    });
3914    workspace.update_in(cx, |_, window, cx| {
3915        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3916    });
3917
3918    // Verify navigation state before clear
3919    workspace.update(cx, |workspace, cx| {
3920        let pane = workspace.active_pane();
3921        pane.read(cx).can_navigate_backward()
3922    });
3923
3924    // Clear navigation history
3925    cx.dispatch_action(workspace::ClearNavigationHistory);
3926
3927    // Verify that navigation is disabled immediately after clear
3928    workspace.update(cx, |workspace, cx| {
3929        let pane = workspace.active_pane();
3930        assert!(
3931            !pane.read(cx).can_navigate_backward(),
3932            "Should not be able to navigate backward after clearing history"
3933        );
3934        assert!(
3935            !pane.read(cx).can_navigate_forward(),
3936            "Should not be able to navigate forward after clearing history"
3937        );
3938    });
3939
3940    // Verify that file finder no longer shows history items
3941    let picker = open_file_picker(&workspace, cx);
3942    cx.simulate_input("fir");
3943    picker.update(cx, |finder, _| {
3944        let matches = collect_search_matches(finder);
3945        assert!(
3946            matches.history.is_empty(),
3947            "File finder should not show history items after clearing"
3948        );
3949    });
3950    workspace.update_in(cx, |_, window, cx| {
3951        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3952    });
3953
3954    // Verify history is empty by opening a new file
3955    // (this should not show any previous history)
3956    let history_after_clear =
3957        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3958    assert_eq!(
3959        history_after_clear.len(),
3960        0,
3961        "Should have no history items after clearing"
3962    );
3963}