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_external_files_history(cx: &mut gpui::TestAppContext) {
1707    let app_state = init_test(cx);
1708
1709    app_state
1710        .fs
1711        .as_fake()
1712        .insert_tree(
1713            path!("/src"),
1714            json!({
1715                "test": {
1716                    "first.rs": "// First Rust file",
1717                    "second.rs": "// Second Rust file",
1718                }
1719            }),
1720        )
1721        .await;
1722
1723    app_state
1724        .fs
1725        .as_fake()
1726        .insert_tree(
1727            path!("/external-src"),
1728            json!({
1729                "test": {
1730                    "third.rs": "// Third Rust file",
1731                    "fourth.rs": "// Fourth Rust file",
1732                }
1733            }),
1734        )
1735        .await;
1736
1737    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1738    cx.update(|cx| {
1739        project.update(cx, |project, cx| {
1740            project.find_or_create_worktree(path!("/external-src"), false, cx)
1741        })
1742    })
1743    .detach();
1744    cx.background_executor.run_until_parked();
1745
1746    let (multi_workspace, cx) =
1747        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1748    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1749    let worktree_id = cx.read(|cx| {
1750        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1751        assert_eq!(worktrees.len(), 1,);
1752
1753        worktrees[0].read(cx).id()
1754    });
1755    workspace
1756        .update_in(cx, |workspace, window, cx| {
1757            workspace.open_abs_path(
1758                PathBuf::from(path!("/external-src/test/third.rs")),
1759                OpenOptions {
1760                    visible: Some(OpenVisible::None),
1761                    ..Default::default()
1762                },
1763                window,
1764                cx,
1765            )
1766        })
1767        .detach();
1768    cx.background_executor.run_until_parked();
1769    let external_worktree_id = cx.read(|cx| {
1770        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1771        assert_eq!(
1772            worktrees.len(),
1773            2,
1774            "External file should get opened in a new worktree"
1775        );
1776
1777        worktrees
1778            .into_iter()
1779            .find(|worktree| worktree.read(cx).id() != worktree_id)
1780            .expect("New worktree should have a different id")
1781            .read(cx)
1782            .id()
1783    });
1784    cx.dispatch_action(workspace::CloseActiveItem {
1785        save_intent: None,
1786        close_pinned: false,
1787    });
1788
1789    let initial_history_items =
1790        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1791    assert_eq!(
1792        initial_history_items,
1793        vec![FoundPath::new(
1794            ProjectPath {
1795                worktree_id: external_worktree_id,
1796                path: rel_path("").into(),
1797            },
1798            PathBuf::from(path!("/external-src/test/third.rs"))
1799        )],
1800        "Should show external file with its full path in the history after it was open"
1801    );
1802
1803    let updated_history_items =
1804        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1805    assert_eq!(
1806        updated_history_items,
1807        vec![
1808            FoundPath::new(
1809                ProjectPath {
1810                    worktree_id,
1811                    path: rel_path("test/second.rs").into(),
1812                },
1813                PathBuf::from(path!("/src/test/second.rs"))
1814            ),
1815            FoundPath::new(
1816                ProjectPath {
1817                    worktree_id: external_worktree_id,
1818                    path: rel_path("").into(),
1819                },
1820                PathBuf::from(path!("/external-src/test/third.rs"))
1821            ),
1822        ],
1823        "Should keep external file with history updates",
1824    );
1825}
1826
1827#[gpui::test]
1828async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1829    let app_state = init_test(cx);
1830
1831    app_state
1832        .fs
1833        .as_fake()
1834        .insert_tree(
1835            path!("/src"),
1836            json!({
1837                "test": {
1838                    "first.rs": "// First Rust file",
1839                    "second.rs": "// Second Rust file",
1840                    "third.rs": "// Third Rust file",
1841                }
1842            }),
1843        )
1844        .await;
1845
1846    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1847    let (multi_workspace, cx) =
1848        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1849    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1850
1851    // generate some history to select from
1852    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1853    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1854    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1855    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1856
1857    for expected_selected_index in 0..current_history.len() {
1858        cx.dispatch_action(ToggleFileFinder::default());
1859        let picker = active_file_picker(&workspace, cx);
1860        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1861        assert_eq!(
1862            selected_index, expected_selected_index,
1863            "Should select the next item in the history"
1864        );
1865    }
1866
1867    cx.dispatch_action(ToggleFileFinder::default());
1868    let selected_index = workspace.update(cx, |workspace, cx| {
1869        workspace
1870            .active_modal::<FileFinder>(cx)
1871            .unwrap()
1872            .read(cx)
1873            .picker
1874            .read(cx)
1875            .delegate
1876            .selected_index()
1877    });
1878    assert_eq!(
1879        selected_index, 0,
1880        "Should wrap around the history and start all over"
1881    );
1882}
1883
1884#[gpui::test]
1885async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1886    let app_state = init_test(cx);
1887
1888    app_state
1889        .fs
1890        .as_fake()
1891        .insert_tree(
1892            path!("/src"),
1893            json!({
1894                "test": {
1895                    "first.rs": "// First Rust file",
1896                    "second.rs": "// Second Rust file",
1897                    "third.rs": "// Third Rust file",
1898                    "fourth.rs": "// Fourth Rust file",
1899                }
1900            }),
1901        )
1902        .await;
1903
1904    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1905    let (multi_workspace, cx) =
1906        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1907    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1908    let worktree_id = cx.read(|cx| {
1909        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1910        assert_eq!(worktrees.len(), 1,);
1911
1912        worktrees[0].read(cx).id()
1913    });
1914
1915    // generate some history to select from
1916    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1917    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1918    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1919    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1920
1921    let finder = open_file_picker(&workspace, cx);
1922    let first_query = "f";
1923    finder
1924        .update_in(cx, |finder, window, cx| {
1925            finder
1926                .delegate
1927                .update_matches(first_query.to_string(), window, cx)
1928        })
1929        .await;
1930    finder.update(cx, |picker, _| {
1931            let matches = collect_search_matches(picker);
1932            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1933            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1934            assert_eq!(history_match, &FoundPath::new(
1935                ProjectPath {
1936                    worktree_id,
1937                    path: rel_path("test/first.rs").into(),
1938                },
1939                PathBuf::from(path!("/src/test/first.rs")),
1940            ));
1941            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1942            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1943        });
1944
1945    let second_query = "fsdasdsa";
1946    let finder = active_file_picker(&workspace, cx);
1947    finder
1948        .update_in(cx, |finder, window, cx| {
1949            finder
1950                .delegate
1951                .update_matches(second_query.to_string(), window, cx)
1952        })
1953        .await;
1954    finder.update(cx, |picker, _| {
1955        assert!(
1956            collect_search_matches(picker)
1957                .search_paths_only()
1958                .is_empty(),
1959            "No search entries should match {second_query}"
1960        );
1961    });
1962
1963    let first_query_again = first_query;
1964
1965    let finder = active_file_picker(&workspace, cx);
1966    finder
1967        .update_in(cx, |finder, window, cx| {
1968            finder
1969                .delegate
1970                .update_matches(first_query_again.to_string(), window, cx)
1971        })
1972        .await;
1973    finder.update(cx, |picker, _| {
1974            let matches = collect_search_matches(picker);
1975            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");
1976            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1977            assert_eq!(history_match, &FoundPath::new(
1978                ProjectPath {
1979                    worktree_id,
1980                    path: rel_path("test/first.rs").into(),
1981                },
1982                PathBuf::from(path!("/src/test/first.rs"))
1983            ));
1984            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1985            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1986        });
1987}
1988
1989#[gpui::test]
1990async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1991    let app_state = init_test(cx);
1992
1993    app_state
1994        .fs
1995        .as_fake()
1996        .insert_tree(
1997            path!("/root"),
1998            json!({
1999                "test": {
2000                    "1_qw": "// First file that matches the query",
2001                    "2_second": "// Second file",
2002                    "3_third": "// Third file",
2003                    "4_fourth": "// Fourth file",
2004                    "5_qwqwqw": "// A file with 3 more matches than the first one",
2005                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
2006                    "7_qwqwqw": "// One more, same amount of query matches as above",
2007                }
2008            }),
2009        )
2010        .await;
2011
2012    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2013    let (multi_workspace, cx) =
2014        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2015    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2016    // generate some history to select from
2017    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2018    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2019    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
2020    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2021    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
2022
2023    let finder = open_file_picker(&workspace, cx);
2024    let query = "qw";
2025    finder
2026        .update_in(cx, |finder, window, cx| {
2027            finder
2028                .delegate
2029                .update_matches(query.to_string(), window, cx)
2030        })
2031        .await;
2032    finder.update(cx, |finder, _| {
2033        let search_matches = collect_search_matches(finder);
2034        assert_eq!(
2035            search_matches.history,
2036            vec![
2037                rel_path("test/1_qw").into(),
2038                rel_path("test/6_qwqwqw").into()
2039            ],
2040        );
2041        assert_eq!(
2042            search_matches.search,
2043            vec![
2044                rel_path("test/5_qwqwqw").into(),
2045                rel_path("test/7_qwqwqw").into()
2046            ],
2047        );
2048    });
2049}
2050
2051#[gpui::test]
2052async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
2053    let app_state = init_test(cx);
2054
2055    app_state
2056        .fs
2057        .as_fake()
2058        .insert_tree(
2059            path!("/root"),
2060            json!({
2061                "test": {
2062                    "1_qw": "",
2063                }
2064            }),
2065        )
2066        .await;
2067
2068    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2069    let (multi_workspace, cx) =
2070        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2071    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2072    // Open new buffer
2073    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2074
2075    let picker = open_file_picker(&workspace, cx);
2076    picker.update(cx, |finder, _| {
2077        assert_match_selection(finder, 0, "1_qw");
2078    });
2079}
2080
2081#[gpui::test]
2082async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
2083    cx: &mut TestAppContext,
2084) {
2085    let app_state = init_test(cx);
2086
2087    app_state
2088        .fs
2089        .as_fake()
2090        .insert_tree(
2091            path!("/src"),
2092            json!({
2093                "test": {
2094                    "bar.rs": "// Bar file",
2095                    "lib.rs": "// Lib file",
2096                    "maaa.rs": "// Maaaaaaa",
2097                    "main.rs": "// Main file",
2098                    "moo.rs": "// Moooooo",
2099                }
2100            }),
2101        )
2102        .await;
2103
2104    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2105    let (multi_workspace, cx) =
2106        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2107    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2108
2109    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2110    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2111    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2112
2113    // main.rs is on top, previously used is selected
2114    let picker = open_file_picker(&workspace, cx);
2115    picker.update(cx, |finder, _| {
2116        assert_eq!(finder.delegate.matches.len(), 3);
2117        assert_match_selection(finder, 0, "main.rs");
2118        assert_match_at_position(finder, 1, "lib.rs");
2119        assert_match_at_position(finder, 2, "bar.rs");
2120    });
2121
2122    // all files match, main.rs is still on top, but the second item is selected
2123    picker
2124        .update_in(cx, |finder, window, cx| {
2125            finder
2126                .delegate
2127                .update_matches(".rs".to_string(), window, cx)
2128        })
2129        .await;
2130    picker.update(cx, |finder, _| {
2131        assert_eq!(finder.delegate.matches.len(), 6);
2132        assert_match_at_position(finder, 0, "main.rs");
2133        assert_match_selection(finder, 1, "bar.rs");
2134        assert_match_at_position(finder, 2, "lib.rs");
2135        assert_match_at_position(finder, 3, "moo.rs");
2136        assert_match_at_position(finder, 4, "maaa.rs");
2137        assert_match_at_position(finder, 5, ".rs");
2138    });
2139
2140    // main.rs is not among matches, select top item
2141    picker
2142        .update_in(cx, |finder, window, cx| {
2143            finder.delegate.update_matches("b".to_string(), window, cx)
2144        })
2145        .await;
2146    picker.update(cx, |finder, _| {
2147        assert_eq!(finder.delegate.matches.len(), 3);
2148        assert_match_at_position(finder, 0, "bar.rs");
2149        assert_match_at_position(finder, 1, "lib.rs");
2150        assert_match_at_position(finder, 2, "b");
2151    });
2152
2153    // main.rs is back, put it on top and select next item
2154    picker
2155        .update_in(cx, |finder, window, cx| {
2156            finder.delegate.update_matches("m".to_string(), window, cx)
2157        })
2158        .await;
2159    picker.update(cx, |finder, _| {
2160        assert_eq!(finder.delegate.matches.len(), 4);
2161        assert_match_at_position(finder, 0, "main.rs");
2162        assert_match_selection(finder, 1, "moo.rs");
2163        assert_match_at_position(finder, 2, "maaa.rs");
2164        assert_match_at_position(finder, 3, "m");
2165    });
2166
2167    // get back to the initial state
2168    picker
2169        .update_in(cx, |finder, window, cx| {
2170            finder.delegate.update_matches("".to_string(), window, cx)
2171        })
2172        .await;
2173    picker.update(cx, |finder, _| {
2174        assert_eq!(finder.delegate.matches.len(), 3);
2175        assert_match_selection(finder, 0, "main.rs");
2176        assert_match_at_position(finder, 1, "lib.rs");
2177        assert_match_at_position(finder, 2, "bar.rs");
2178    });
2179}
2180
2181#[gpui::test]
2182async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
2183    let app_state = init_test(cx);
2184
2185    cx.update(|cx| {
2186        let settings = *FileFinderSettings::get_global(cx);
2187
2188        FileFinderSettings::override_global(
2189            FileFinderSettings {
2190                skip_focus_for_active_in_search: false,
2191                ..settings
2192            },
2193            cx,
2194        );
2195    });
2196
2197    app_state
2198        .fs
2199        .as_fake()
2200        .insert_tree(
2201            path!("/src"),
2202            json!({
2203                "test": {
2204                    "bar.rs": "// Bar file",
2205                    "lib.rs": "// Lib file",
2206                    "maaa.rs": "// Maaaaaaa",
2207                    "main.rs": "// Main file",
2208                    "moo.rs": "// Moooooo",
2209                }
2210            }),
2211        )
2212        .await;
2213
2214    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2215    let (multi_workspace, cx) =
2216        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2217    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2218
2219    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2220    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2221    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2222
2223    // main.rs is on top, previously used is selected
2224    let picker = open_file_picker(&workspace, cx);
2225    picker.update(cx, |finder, _| {
2226        assert_eq!(finder.delegate.matches.len(), 3);
2227        assert_match_selection(finder, 0, "main.rs");
2228        assert_match_at_position(finder, 1, "lib.rs");
2229        assert_match_at_position(finder, 2, "bar.rs");
2230    });
2231
2232    // all files match, main.rs is on top, and is selected
2233    picker
2234        .update_in(cx, |finder, window, cx| {
2235            finder
2236                .delegate
2237                .update_matches(".rs".to_string(), window, cx)
2238        })
2239        .await;
2240    picker.update(cx, |finder, _| {
2241        assert_eq!(finder.delegate.matches.len(), 6);
2242        assert_match_selection(finder, 0, "main.rs");
2243        assert_match_at_position(finder, 1, "bar.rs");
2244        assert_match_at_position(finder, 2, "lib.rs");
2245        assert_match_at_position(finder, 3, "moo.rs");
2246        assert_match_at_position(finder, 4, "maaa.rs");
2247        assert_match_at_position(finder, 5, ".rs");
2248    });
2249}
2250
2251#[gpui::test]
2252async fn test_non_separate_history_items(cx: &mut TestAppContext) {
2253    let app_state = init_test(cx);
2254
2255    app_state
2256        .fs
2257        .as_fake()
2258        .insert_tree(
2259            path!("/src"),
2260            json!({
2261                "test": {
2262                    "bar.rs": "// Bar file",
2263                    "lib.rs": "// Lib file",
2264                    "maaa.rs": "// Maaaaaaa",
2265                    "main.rs": "// Main file",
2266                    "moo.rs": "// Moooooo",
2267                }
2268            }),
2269        )
2270        .await;
2271
2272    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2273    let (multi_workspace, cx) =
2274        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2275    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2276
2277    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2278    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2279    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2280
2281    cx.dispatch_action(ToggleFileFinder::default());
2282    let picker = active_file_picker(&workspace, cx);
2283    // main.rs is on top, previously used is selected
2284    picker.update(cx, |finder, _| {
2285        assert_eq!(finder.delegate.matches.len(), 3);
2286        assert_match_selection(finder, 0, "main.rs");
2287        assert_match_at_position(finder, 1, "lib.rs");
2288        assert_match_at_position(finder, 2, "bar.rs");
2289    });
2290
2291    // all files match, main.rs is still on top, but the second item is selected
2292    picker
2293        .update_in(cx, |finder, window, cx| {
2294            finder
2295                .delegate
2296                .update_matches(".rs".to_string(), window, cx)
2297        })
2298        .await;
2299    picker.update(cx, |finder, _| {
2300        assert_eq!(finder.delegate.matches.len(), 6);
2301        assert_match_at_position(finder, 0, "main.rs");
2302        assert_match_selection(finder, 1, "moo.rs");
2303        assert_match_at_position(finder, 2, "bar.rs");
2304        assert_match_at_position(finder, 3, "lib.rs");
2305        assert_match_at_position(finder, 4, "maaa.rs");
2306        assert_match_at_position(finder, 5, ".rs");
2307    });
2308
2309    // main.rs is not among matches, select top item
2310    picker
2311        .update_in(cx, |finder, window, cx| {
2312            finder.delegate.update_matches("b".to_string(), window, cx)
2313        })
2314        .await;
2315    picker.update(cx, |finder, _| {
2316        assert_eq!(finder.delegate.matches.len(), 3);
2317        assert_match_at_position(finder, 0, "bar.rs");
2318        assert_match_at_position(finder, 1, "lib.rs");
2319        assert_match_at_position(finder, 2, "b");
2320    });
2321
2322    // main.rs is back, put it on top and select next item
2323    picker
2324        .update_in(cx, |finder, window, cx| {
2325            finder.delegate.update_matches("m".to_string(), window, cx)
2326        })
2327        .await;
2328    picker.update(cx, |finder, _| {
2329        assert_eq!(finder.delegate.matches.len(), 4);
2330        assert_match_at_position(finder, 0, "main.rs");
2331        assert_match_selection(finder, 1, "moo.rs");
2332        assert_match_at_position(finder, 2, "maaa.rs");
2333        assert_match_at_position(finder, 3, "m");
2334    });
2335
2336    // get back to the initial state
2337    picker
2338        .update_in(cx, |finder, window, cx| {
2339            finder.delegate.update_matches("".to_string(), window, cx)
2340        })
2341        .await;
2342    picker.update(cx, |finder, _| {
2343        assert_eq!(finder.delegate.matches.len(), 3);
2344        assert_match_selection(finder, 0, "main.rs");
2345        assert_match_at_position(finder, 1, "lib.rs");
2346        assert_match_at_position(finder, 2, "bar.rs");
2347    });
2348}
2349
2350#[gpui::test]
2351async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
2352    let app_state = init_test(cx);
2353
2354    app_state
2355        .fs
2356        .as_fake()
2357        .insert_tree(
2358            path!("/test"),
2359            json!({
2360                "test": {
2361                    "1.txt": "// One",
2362                    "2.txt": "// Two",
2363                    "3.txt": "// Three",
2364                }
2365            }),
2366        )
2367        .await;
2368
2369    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2370    let (multi_workspace, cx) =
2371        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2372    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2373
2374    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2375    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2376    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2377
2378    let picker = open_file_picker(&workspace, cx);
2379    picker.update(cx, |finder, _| {
2380        assert_eq!(finder.delegate.matches.len(), 3);
2381        assert_match_selection(finder, 0, "3.txt");
2382        assert_match_at_position(finder, 1, "2.txt");
2383        assert_match_at_position(finder, 2, "1.txt");
2384    });
2385
2386    cx.dispatch_action(SelectNext);
2387    cx.dispatch_action(Confirm); // Open 2.txt
2388
2389    let picker = open_file_picker(&workspace, cx);
2390    picker.update(cx, |finder, _| {
2391        assert_eq!(finder.delegate.matches.len(), 3);
2392        assert_match_selection(finder, 0, "2.txt");
2393        assert_match_at_position(finder, 1, "3.txt");
2394        assert_match_at_position(finder, 2, "1.txt");
2395    });
2396
2397    cx.dispatch_action(SelectNext);
2398    cx.dispatch_action(SelectNext);
2399    cx.dispatch_action(Confirm); // Open 1.txt
2400
2401    let picker = open_file_picker(&workspace, cx);
2402    picker.update(cx, |finder, _| {
2403        assert_eq!(finder.delegate.matches.len(), 3);
2404        assert_match_selection(finder, 0, "1.txt");
2405        assert_match_at_position(finder, 1, "2.txt");
2406        assert_match_at_position(finder, 2, "3.txt");
2407    });
2408}
2409
2410#[gpui::test]
2411async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2412    let app_state = init_test(cx);
2413
2414    app_state
2415        .fs
2416        .as_fake()
2417        .insert_tree(
2418            path!("/test"),
2419            json!({
2420                "test": {
2421                    "1.txt": "// One",
2422                    "2.txt": "// Two",
2423                    "3.txt": "// Three",
2424                }
2425            }),
2426        )
2427        .await;
2428
2429    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2430    let (multi_workspace, cx) =
2431        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2432    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2433
2434    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2435    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2436    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2437
2438    let picker = open_file_picker(&workspace, cx);
2439    picker.update(cx, |finder, _| {
2440        assert_eq!(finder.delegate.matches.len(), 3);
2441        assert_match_selection(finder, 0, "3.txt");
2442        assert_match_at_position(finder, 1, "2.txt");
2443        assert_match_at_position(finder, 2, "1.txt");
2444    });
2445
2446    cx.dispatch_action(SelectNext);
2447
2448    // Add more files to the worktree to trigger update matches
2449    for i in 0..5 {
2450        let filename = if cfg!(windows) {
2451            format!("C:/test/{}.txt", 4 + i)
2452        } else {
2453            format!("/test/{}.txt", 4 + i)
2454        };
2455        app_state
2456            .fs
2457            .create_file(Path::new(&filename), Default::default())
2458            .await
2459            .expect("unable to create file");
2460    }
2461
2462    cx.executor().advance_clock(FS_WATCH_LATENCY);
2463
2464    picker.update(cx, |finder, _| {
2465        assert_eq!(finder.delegate.matches.len(), 3);
2466        assert_match_at_position(finder, 0, "3.txt");
2467        assert_match_selection(finder, 1, "2.txt");
2468        assert_match_at_position(finder, 2, "1.txt");
2469    });
2470}
2471
2472#[gpui::test]
2473async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2474    let app_state = init_test(cx);
2475
2476    app_state
2477        .fs
2478        .as_fake()
2479        .insert_tree(
2480            path!("/src"),
2481            json!({
2482                "collab_ui": {
2483                    "first.rs": "// First Rust file",
2484                    "second.rs": "// Second Rust file",
2485                    "third.rs": "// Third Rust file",
2486                    "collab_ui.rs": "// Fourth Rust file",
2487                }
2488            }),
2489        )
2490        .await;
2491
2492    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2493    let (multi_workspace, cx) =
2494        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2495    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2496    // generate some history to select from
2497    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2498    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2499    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2500    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2501
2502    let finder = open_file_picker(&workspace, cx);
2503    let query = "collab_ui";
2504    cx.simulate_input(query);
2505    finder.update(cx, |picker, _| {
2506            let search_entries = collect_search_matches(picker).search_paths_only();
2507            assert_eq!(
2508                search_entries,
2509                vec![
2510                    rel_path("collab_ui/collab_ui.rs").into(),
2511                    rel_path("collab_ui/first.rs").into(),
2512                    rel_path("collab_ui/third.rs").into(),
2513                    rel_path("collab_ui/second.rs").into(),
2514                ],
2515                "Despite all search results having the same directory name, the most matching one should be on top"
2516            );
2517        });
2518}
2519
2520#[gpui::test]
2521async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2522    let app_state = init_test(cx);
2523
2524    app_state
2525        .fs
2526        .as_fake()
2527        .insert_tree(
2528            path!("/src"),
2529            json!({
2530                "test": {
2531                    "first.rs": "// First Rust file",
2532                    "nonexistent.rs": "// Second Rust file",
2533                    "third.rs": "// Third Rust file",
2534                }
2535            }),
2536        )
2537        .await;
2538
2539    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2540    let (multi_workspace, cx) =
2541        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); // generate some history to select from
2542    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2543    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2544    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2545    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2546    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2547    app_state
2548        .fs
2549        .remove_file(
2550            Path::new(path!("/src/test/nonexistent.rs")),
2551            RemoveOptions::default(),
2552        )
2553        .await
2554        .unwrap();
2555    cx.run_until_parked();
2556
2557    let picker = open_file_picker(&workspace, cx);
2558    cx.simulate_input("rs");
2559
2560    picker.update(cx, |picker, _| {
2561        assert_eq!(
2562            collect_search_matches(picker).history,
2563            vec![
2564                rel_path("test/first.rs").into(),
2565                rel_path("test/third.rs").into()
2566            ],
2567            "Should have all opened files in the history, except the ones that do not exist on disk"
2568        );
2569    });
2570}
2571
2572#[gpui::test]
2573async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2574    let app_state = init_test(cx);
2575
2576    app_state
2577        .fs
2578        .as_fake()
2579        .insert_tree(
2580            "/src",
2581            json!({
2582                "lib.rs": "// Lib file",
2583                "main.rs": "// Bar file",
2584                "read.me": "// Readme file",
2585            }),
2586        )
2587        .await;
2588
2589    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2590    let (multi_workspace, cx) =
2591        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2592    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2593
2594    // Initial state
2595    let picker = open_file_picker(&workspace, cx);
2596    cx.simulate_input("rs");
2597    picker.update(cx, |finder, _| {
2598        assert_eq!(finder.delegate.matches.len(), 3);
2599        assert_match_at_position(finder, 0, "lib.rs");
2600        assert_match_at_position(finder, 1, "main.rs");
2601        assert_match_at_position(finder, 2, "rs");
2602    });
2603    // Delete main.rs
2604    app_state
2605        .fs
2606        .remove_file("/src/main.rs".as_ref(), Default::default())
2607        .await
2608        .expect("unable to remove file");
2609    cx.executor().advance_clock(FS_WATCH_LATENCY);
2610
2611    // main.rs is in not among search results anymore
2612    picker.update(cx, |finder, _| {
2613        assert_eq!(finder.delegate.matches.len(), 2);
2614        assert_match_at_position(finder, 0, "lib.rs");
2615        assert_match_at_position(finder, 1, "rs");
2616    });
2617
2618    // Create util.rs
2619    app_state
2620        .fs
2621        .create_file("/src/util.rs".as_ref(), Default::default())
2622        .await
2623        .expect("unable to create file");
2624    cx.executor().advance_clock(FS_WATCH_LATENCY);
2625
2626    // util.rs is among search results
2627    picker.update(cx, |finder, _| {
2628        assert_eq!(finder.delegate.matches.len(), 3);
2629        assert_match_at_position(finder, 0, "lib.rs");
2630        assert_match_at_position(finder, 1, "util.rs");
2631        assert_match_at_position(finder, 2, "rs");
2632    });
2633}
2634
2635#[gpui::test]
2636async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
2637    let app_state = init_test(cx);
2638
2639    app_state
2640        .fs
2641        .as_fake()
2642        .insert_tree(
2643            "/src",
2644            json!({
2645                "lib.rs": "// Lib file",
2646                "main.rs": "// Bar file",
2647                "read.me": "// Readme file",
2648            }),
2649        )
2650        .await;
2651    app_state
2652        .fs
2653        .as_fake()
2654        .insert_tree(
2655            "/test",
2656            json!({
2657                "new.rs": "// New file",
2658            }),
2659        )
2660        .await;
2661
2662    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2663    let window = cx.add_window({
2664        let project = project.clone();
2665        |window, cx| MultiWorkspace::test_new(project, window, cx)
2666    });
2667    let cx = VisualTestContext::from_window(*window, cx).into_mut();
2668    let workspace = window
2669        .read_with(cx, |mw, _| mw.workspace().clone())
2670        .unwrap();
2671
2672    cx.update(|_, cx| {
2673        open_paths(
2674            &[PathBuf::from(path!("/test/new.rs"))],
2675            app_state,
2676            workspace::OpenOptions::default(),
2677            cx,
2678        )
2679    })
2680    .await
2681    .unwrap();
2682    assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
2683
2684    let initial_history = open_close_queried_buffer("new", 1, "new.rs", &workspace, cx).await;
2685    assert_eq!(
2686        initial_history.first().unwrap().absolute,
2687        PathBuf::from(path!("/test/new.rs")),
2688        "Should show 1st opened item in the history when opening the 2nd item"
2689    );
2690
2691    let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2692    assert_eq!(
2693        history_after_first.first().unwrap().absolute,
2694        PathBuf::from(path!("/test/new.rs")),
2695        "Should show 1st opened item in the history when opening the 2nd item"
2696    );
2697}
2698
2699#[gpui::test]
2700async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2701    cx: &mut gpui::TestAppContext,
2702) {
2703    let app_state = init_test(cx);
2704
2705    app_state
2706        .fs
2707        .as_fake()
2708        .insert_tree(
2709            "/test",
2710            json!({
2711                "project_1": {
2712                    "bar.rs": "// Bar file",
2713                    "lib.rs": "// Lib file",
2714                },
2715                "project_2": {
2716                    "Cargo.toml": "// Cargo file",
2717                    "main.rs": "// Main file",
2718                }
2719            }),
2720        )
2721        .await;
2722
2723    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2724    let (multi_workspace, cx) =
2725        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2726    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2727    let worktree_1_id = project.update(cx, |project, cx| {
2728        let worktree = project.worktrees(cx).last().expect("worktree not found");
2729        worktree.read(cx).id()
2730    });
2731
2732    // Initial state
2733    let picker = open_file_picker(&workspace, cx);
2734    cx.simulate_input("rs");
2735    picker.update(cx, |finder, _| {
2736        assert_eq!(finder.delegate.matches.len(), 3);
2737        assert_match_at_position(finder, 0, "bar.rs");
2738        assert_match_at_position(finder, 1, "lib.rs");
2739        assert_match_at_position(finder, 2, "rs");
2740    });
2741
2742    // Add new worktree
2743    project
2744        .update(cx, |project, cx| {
2745            project
2746                .find_or_create_worktree("/test/project_2", true, cx)
2747                .into_future()
2748        })
2749        .await
2750        .expect("unable to create workdir");
2751    cx.executor().advance_clock(FS_WATCH_LATENCY);
2752
2753    // main.rs is among search results
2754    picker.update(cx, |finder, _| {
2755        assert_eq!(finder.delegate.matches.len(), 4);
2756        assert_match_at_position(finder, 0, "bar.rs");
2757        assert_match_at_position(finder, 1, "lib.rs");
2758        assert_match_at_position(finder, 2, "main.rs");
2759        assert_match_at_position(finder, 3, "rs");
2760    });
2761
2762    // Remove the first worktree
2763    project.update(cx, |project, cx| {
2764        project.remove_worktree(worktree_1_id, cx);
2765    });
2766    cx.executor().advance_clock(FS_WATCH_LATENCY);
2767
2768    // Files from the first worktree are not in the search results anymore
2769    picker.update(cx, |finder, _| {
2770        assert_eq!(finder.delegate.matches.len(), 2);
2771        assert_match_at_position(finder, 0, "main.rs");
2772        assert_match_at_position(finder, 1, "rs");
2773    });
2774}
2775
2776#[gpui::test]
2777async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
2778    cx: &mut TestAppContext,
2779) {
2780    let app_state = init_test(cx);
2781    app_state
2782        .fs
2783        .as_fake()
2784        .insert_tree(
2785            path!("/repo1"),
2786            json!({
2787                "package.json": r#"{"name": "repo1"}"#,
2788                "src": {
2789                    "index.js": "// Repo 1 index",
2790                }
2791            }),
2792        )
2793        .await;
2794
2795    app_state
2796        .fs
2797        .as_fake()
2798        .insert_tree(
2799            path!("/repo2"),
2800            json!({
2801                "package.json": r#"{"name": "repo2"}"#,
2802                "src": {
2803                    "index.js": "// Repo 2 index",
2804                }
2805            }),
2806        )
2807        .await;
2808
2809    let project = Project::test(
2810        app_state.fs.clone(),
2811        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
2812        cx,
2813    )
2814    .await;
2815
2816    let (multi_workspace, cx) =
2817        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2818    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2819    let (worktree_id1, worktree_id2) = cx.read(|cx| {
2820        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2821        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
2822    });
2823
2824    workspace
2825        .update_in(cx, |workspace, window, cx| {
2826            workspace.open_path(
2827                ProjectPath {
2828                    worktree_id: worktree_id1,
2829                    path: rel_path("package.json").into(),
2830                },
2831                None,
2832                true,
2833                window,
2834                cx,
2835            )
2836        })
2837        .await
2838        .unwrap();
2839
2840    cx.dispatch_action(workspace::CloseActiveItem {
2841        save_intent: None,
2842        close_pinned: false,
2843    });
2844    workspace
2845        .update_in(cx, |workspace, window, cx| {
2846            workspace.open_path(
2847                ProjectPath {
2848                    worktree_id: worktree_id2,
2849                    path: rel_path("package.json").into(),
2850                },
2851                None,
2852                true,
2853                window,
2854                cx,
2855            )
2856        })
2857        .await
2858        .unwrap();
2859
2860    cx.dispatch_action(workspace::CloseActiveItem {
2861        save_intent: None,
2862        close_pinned: false,
2863    });
2864
2865    let picker = open_file_picker(&workspace, cx);
2866    cx.simulate_input("package.json");
2867
2868    picker.update(cx, |finder, _| {
2869        let matches = &finder.delegate.matches.matches;
2870
2871        assert_eq!(
2872            matches.len(),
2873            2,
2874            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
2875            matches.len(),
2876            matches
2877        );
2878
2879        assert_matches!(matches[0], Match::History { .. });
2880
2881        let search_matches = collect_search_matches(finder);
2882        assert_eq!(
2883            search_matches.history.len(),
2884            2,
2885            "Should have exactly 2 history match"
2886        );
2887        assert_eq!(
2888            search_matches.search.len(),
2889            0,
2890            "Should have exactly 0 search match (because we already opened the 2 package.json)"
2891        );
2892
2893        if let Match::History { path, panel_match } = &matches[0] {
2894            assert_eq!(path.project.worktree_id, worktree_id2);
2895            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2896            let panel_match = panel_match.as_ref().unwrap();
2897            assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
2898            assert_eq!(panel_match.0.path, rel_path("package.json").into());
2899            assert_eq!(
2900                panel_match.0.positions,
2901                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2902            );
2903        }
2904
2905        if let Match::History { path, panel_match } = &matches[1] {
2906            assert_eq!(path.project.worktree_id, worktree_id1);
2907            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2908            let panel_match = panel_match.as_ref().unwrap();
2909            assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
2910            assert_eq!(panel_match.0.path, rel_path("package.json").into());
2911            assert_eq!(
2912                panel_match.0.positions,
2913                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2914            );
2915        }
2916    });
2917}
2918
2919#[gpui::test]
2920async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2921    let app_state = init_test(cx);
2922
2923    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2924
2925    app_state
2926        .fs
2927        .create_dir("/src/even".as_ref())
2928        .await
2929        .expect("unable to create dir");
2930
2931    let initial_files_num = 5;
2932    for i in 0..initial_files_num {
2933        let filename = format!("/src/even/file_{}.txt", 10 + i);
2934        app_state
2935            .fs
2936            .create_file(Path::new(&filename), Default::default())
2937            .await
2938            .expect("unable to create file");
2939    }
2940
2941    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2942    let (multi_workspace, cx) =
2943        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2944    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2945
2946    // Initial state
2947    let picker = open_file_picker(&workspace, cx);
2948    cx.simulate_input("file");
2949    let selected_index = 3;
2950    // Checking only the filename, not the whole path
2951    let selected_file = format!("file_{}.txt", 10 + selected_index);
2952    // Select even/file_13.txt
2953    for _ in 0..selected_index {
2954        cx.dispatch_action(SelectNext);
2955    }
2956
2957    picker.update(cx, |finder, _| {
2958        assert_match_selection(finder, selected_index, &selected_file)
2959    });
2960
2961    // Add more matches to the search results
2962    let files_to_add = 10;
2963    for i in 0..files_to_add {
2964        let filename = format!("/src/file_{}.txt", 20 + i);
2965        app_state
2966            .fs
2967            .create_file(Path::new(&filename), Default::default())
2968            .await
2969            .expect("unable to create file");
2970        // Wait for each file system event to be fully processed before adding the next
2971        cx.executor().advance_clock(FS_WATCH_LATENCY);
2972        cx.run_until_parked();
2973    }
2974
2975    // file_13.txt is still selected
2976    picker.update(cx, |finder, _| {
2977        let expected_selected_index = selected_index + files_to_add;
2978        assert_match_selection(finder, expected_selected_index, &selected_file);
2979    });
2980}
2981
2982#[gpui::test]
2983async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2984    cx: &mut gpui::TestAppContext,
2985) {
2986    let app_state = init_test(cx);
2987
2988    app_state
2989        .fs
2990        .as_fake()
2991        .insert_tree(
2992            "/src",
2993            json!({
2994                "file_1.txt": "// file_1",
2995                "file_2.txt": "// file_2",
2996                "file_3.txt": "// file_3",
2997            }),
2998        )
2999        .await;
3000
3001    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3002    let (multi_workspace, cx) =
3003        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3004    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3005
3006    // Initial state
3007    let picker = open_file_picker(&workspace, cx);
3008    cx.simulate_input("file");
3009    // Select even/file_2.txt
3010    cx.dispatch_action(SelectNext);
3011
3012    // Remove the selected entry
3013    app_state
3014        .fs
3015        .remove_file("/src/file_2.txt".as_ref(), Default::default())
3016        .await
3017        .expect("unable to remove file");
3018    cx.executor().advance_clock(FS_WATCH_LATENCY);
3019
3020    // file_1.txt is now selected
3021    picker.update(cx, |finder, _| {
3022        assert_match_selection(finder, 0, "file_1.txt");
3023    });
3024}
3025
3026#[gpui::test]
3027async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
3028    let app_state = init_test(cx);
3029
3030    app_state
3031        .fs
3032        .as_fake()
3033        .insert_tree(
3034            path!("/test"),
3035            json!({
3036                "1.txt": "// One",
3037            }),
3038        )
3039        .await;
3040
3041    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3042    let (multi_workspace, cx) =
3043        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3044    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3045
3046    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3047
3048    cx.simulate_modifiers_change(Modifiers::secondary_key());
3049    open_file_picker(&workspace, cx);
3050
3051    cx.simulate_modifiers_change(Modifiers::none());
3052    active_file_picker(&workspace, cx);
3053}
3054
3055#[gpui::test]
3056async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
3057    let app_state = init_test(cx);
3058
3059    app_state
3060        .fs
3061        .as_fake()
3062        .insert_tree(
3063            path!("/test"),
3064            json!({
3065                "1.txt": "// One",
3066                "2.txt": "// Two",
3067            }),
3068        )
3069        .await;
3070
3071    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3072    let (multi_workspace, cx) =
3073        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3074    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3075
3076    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3077    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3078
3079    cx.simulate_modifiers_change(Modifiers::secondary_key());
3080    let picker = open_file_picker(&workspace, cx);
3081    picker.update(cx, |finder, _| {
3082        assert_eq!(finder.delegate.matches.len(), 2);
3083        assert_match_selection(finder, 0, "2.txt");
3084        assert_match_at_position(finder, 1, "1.txt");
3085    });
3086
3087    cx.dispatch_action(SelectNext);
3088    cx.simulate_modifiers_change(Modifiers::none());
3089    cx.read(|cx| {
3090        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3091        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
3092    });
3093}
3094
3095#[gpui::test]
3096async fn test_switches_between_release_norelease_modes_on_forward_nav(
3097    cx: &mut gpui::TestAppContext,
3098) {
3099    let app_state = init_test(cx);
3100
3101    app_state
3102        .fs
3103        .as_fake()
3104        .insert_tree(
3105            path!("/test"),
3106            json!({
3107                "1.txt": "// One",
3108                "2.txt": "// Two",
3109            }),
3110        )
3111        .await;
3112
3113    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3114    let (multi_workspace, cx) =
3115        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3116    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3117
3118    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3119    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3120
3121    // Open with a shortcut
3122    cx.simulate_modifiers_change(Modifiers::secondary_key());
3123    let picker = open_file_picker(&workspace, cx);
3124    picker.update(cx, |finder, _| {
3125        assert_eq!(finder.delegate.matches.len(), 2);
3126        assert_match_selection(finder, 0, "2.txt");
3127        assert_match_at_position(finder, 1, "1.txt");
3128    });
3129
3130    // Switch to navigating with other shortcuts
3131    // Don't open file on modifiers release
3132    cx.simulate_modifiers_change(Modifiers::control());
3133    cx.dispatch_action(SelectNext);
3134    cx.simulate_modifiers_change(Modifiers::none());
3135    picker.update(cx, |finder, _| {
3136        assert_eq!(finder.delegate.matches.len(), 2);
3137        assert_match_at_position(finder, 0, "2.txt");
3138        assert_match_selection(finder, 1, "1.txt");
3139    });
3140
3141    // Back to navigation with initial shortcut
3142    // Open file on modifiers release
3143    cx.simulate_modifiers_change(Modifiers::secondary_key());
3144    cx.dispatch_action(ToggleFileFinder::default());
3145    cx.simulate_modifiers_change(Modifiers::none());
3146    cx.read(|cx| {
3147        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3148        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
3149    });
3150}
3151
3152#[gpui::test]
3153async fn test_switches_between_release_norelease_modes_on_backward_nav(
3154    cx: &mut gpui::TestAppContext,
3155) {
3156    let app_state = init_test(cx);
3157
3158    app_state
3159        .fs
3160        .as_fake()
3161        .insert_tree(
3162            path!("/test"),
3163            json!({
3164                "1.txt": "// One",
3165                "2.txt": "// Two",
3166                "3.txt": "// Three"
3167            }),
3168        )
3169        .await;
3170
3171    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3172    let (multi_workspace, cx) =
3173        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3174    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3175
3176    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3177    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3178    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
3179
3180    // Open with a shortcut
3181    cx.simulate_modifiers_change(Modifiers::secondary_key());
3182    let picker = open_file_picker(&workspace, cx);
3183    picker.update(cx, |finder, _| {
3184        assert_eq!(finder.delegate.matches.len(), 3);
3185        assert_match_selection(finder, 0, "3.txt");
3186        assert_match_at_position(finder, 1, "2.txt");
3187        assert_match_at_position(finder, 2, "1.txt");
3188    });
3189
3190    // Switch to navigating with other shortcuts
3191    // Don't open file on modifiers release
3192    cx.simulate_modifiers_change(Modifiers::control());
3193    cx.dispatch_action(menu::SelectPrevious);
3194    cx.simulate_modifiers_change(Modifiers::none());
3195    picker.update(cx, |finder, _| {
3196        assert_eq!(finder.delegate.matches.len(), 3);
3197        assert_match_at_position(finder, 0, "3.txt");
3198        assert_match_at_position(finder, 1, "2.txt");
3199        assert_match_selection(finder, 2, "1.txt");
3200    });
3201
3202    // Back to navigation with initial shortcut
3203    // Open file on modifiers release
3204    cx.simulate_modifiers_change(Modifiers::secondary_key());
3205    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
3206    cx.simulate_modifiers_change(Modifiers::none());
3207    cx.read(|cx| {
3208        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3209        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
3210    });
3211}
3212
3213#[gpui::test]
3214async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
3215    let app_state = init_test(cx);
3216
3217    app_state
3218        .fs
3219        .as_fake()
3220        .insert_tree(
3221            path!("/test"),
3222            json!({
3223                "1.txt": "// One",
3224            }),
3225        )
3226        .await;
3227
3228    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3229    let (multi_workspace, cx) =
3230        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3231    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3232
3233    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3234
3235    cx.simulate_modifiers_change(Modifiers::secondary_key());
3236    open_file_picker(&workspace, cx);
3237
3238    cx.simulate_modifiers_change(Modifiers::command_shift());
3239    active_file_picker(&workspace, cx);
3240}
3241
3242#[gpui::test]
3243async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
3244    let app_state = init_test(cx);
3245    app_state
3246        .fs
3247        .as_fake()
3248        .insert_tree(
3249            "/test",
3250            json!({
3251                "00.txt": "",
3252                "01.txt": "",
3253                "02.txt": "",
3254                "03.txt": "",
3255                "04.txt": "",
3256                "05.txt": "",
3257            }),
3258        )
3259        .await;
3260
3261    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
3262    let (multi_workspace, cx) =
3263        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3264    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3265
3266    cx.dispatch_action(ToggleFileFinder::default());
3267    let picker = active_file_picker(&workspace, cx);
3268
3269    picker.update_in(cx, |picker, window, cx| {
3270        picker.update_matches(".txt".to_string(), window, cx)
3271    });
3272
3273    cx.run_until_parked();
3274
3275    picker.update(cx, |picker, _| {
3276        assert_eq!(picker.delegate.matches.len(), 7);
3277        assert_eq!(picker.delegate.selected_index, 0);
3278    });
3279
3280    // When toggling repeatedly, the picker scrolls to reveal the selected item.
3281    cx.dispatch_action(ToggleFileFinder::default());
3282    cx.dispatch_action(ToggleFileFinder::default());
3283    cx.dispatch_action(ToggleFileFinder::default());
3284
3285    cx.run_until_parked();
3286
3287    picker.update(cx, |picker, _| {
3288        assert_eq!(picker.delegate.matches.len(), 7);
3289        assert_eq!(picker.delegate.selected_index, 3);
3290    });
3291}
3292
3293async fn open_close_queried_buffer(
3294    input: &str,
3295    expected_matches: usize,
3296    expected_editor_title: &str,
3297    workspace: &Entity<Workspace>,
3298    cx: &mut gpui::VisualTestContext,
3299) -> Vec<FoundPath> {
3300    let history_items = open_queried_buffer(
3301        input,
3302        expected_matches,
3303        expected_editor_title,
3304        workspace,
3305        cx,
3306    )
3307    .await;
3308
3309    cx.dispatch_action(workspace::CloseActiveItem {
3310        save_intent: None,
3311        close_pinned: false,
3312    });
3313
3314    history_items
3315}
3316
3317async fn open_queried_buffer(
3318    input: &str,
3319    expected_matches: usize,
3320    expected_editor_title: &str,
3321    workspace: &Entity<Workspace>,
3322    cx: &mut gpui::VisualTestContext,
3323) -> Vec<FoundPath> {
3324    let picker = open_file_picker(workspace, cx);
3325    cx.simulate_input(input);
3326
3327    let history_items = picker.update(cx, |finder, _| {
3328        assert_eq!(
3329            finder.delegate.matches.len(),
3330            expected_matches + 1, // +1 from CreateNew option
3331            "Unexpected number of matches found for query `{input}`, matches: {:?}",
3332            finder.delegate.matches
3333        );
3334        finder.delegate.history_items.clone()
3335    });
3336
3337    cx.dispatch_action(Confirm);
3338
3339    cx.read(|cx| {
3340        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3341        let active_editor_title = active_editor.read(cx).title(cx);
3342        assert_eq!(
3343            expected_editor_title, active_editor_title,
3344            "Unexpected editor title for query `{input}`"
3345        );
3346    });
3347
3348    history_items
3349}
3350
3351fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3352    cx.update(|cx| {
3353        let state = AppState::test(cx);
3354        theme::init(theme::LoadThemes::JustBase, cx);
3355        super::init(cx);
3356        editor::init(cx);
3357        state
3358    })
3359}
3360
3361fn test_path_position(test_str: &str) -> FileSearchQuery {
3362    let path_position = PathWithPosition::parse_str(test_str);
3363
3364    FileSearchQuery {
3365        raw_query: test_str.to_owned(),
3366        file_query_end: if path_position.path.to_str().unwrap() == test_str {
3367            None
3368        } else {
3369            Some(path_position.path.to_str().unwrap().len())
3370        },
3371        path_position,
3372    }
3373}
3374
3375fn build_find_picker(
3376    project: Entity<Project>,
3377    cx: &mut TestAppContext,
3378) -> (
3379    Entity<Picker<FileFinderDelegate>>,
3380    Entity<Workspace>,
3381    &mut VisualTestContext,
3382) {
3383    let (multi_workspace, cx) =
3384        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3385    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3386    let picker = open_file_picker(&workspace, cx);
3387    (picker, workspace, cx)
3388}
3389
3390#[track_caller]
3391fn open_file_picker(
3392    workspace: &Entity<Workspace>,
3393    cx: &mut VisualTestContext,
3394) -> Entity<Picker<FileFinderDelegate>> {
3395    cx.dispatch_action(ToggleFileFinder {
3396        separate_history: true,
3397    });
3398    active_file_picker(workspace, cx)
3399}
3400
3401#[track_caller]
3402fn active_file_picker(
3403    workspace: &Entity<Workspace>,
3404    cx: &mut VisualTestContext,
3405) -> Entity<Picker<FileFinderDelegate>> {
3406    workspace.update(cx, |workspace, cx| {
3407        workspace
3408            .active_modal::<FileFinder>(cx)
3409            .expect("file finder is not open")
3410            .read(cx)
3411            .picker
3412            .clone()
3413    })
3414}
3415
3416#[derive(Debug, Default)]
3417struct SearchEntries {
3418    history: Vec<Arc<RelPath>>,
3419    history_found_paths: Vec<FoundPath>,
3420    search: Vec<Arc<RelPath>>,
3421    search_matches: Vec<PathMatch>,
3422}
3423
3424impl SearchEntries {
3425    #[track_caller]
3426    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
3427        assert!(
3428            self.history.is_empty(),
3429            "Should have no history matches, but got: {:?}",
3430            self.history
3431        );
3432        self.search
3433    }
3434
3435    #[track_caller]
3436    fn search_matches_only(self) -> Vec<PathMatch> {
3437        assert!(
3438            self.history.is_empty(),
3439            "Should have no history matches, but got: {:?}",
3440            self.history
3441        );
3442        self.search_matches
3443    }
3444}
3445
3446fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
3447    let mut search_entries = SearchEntries::default();
3448    for m in &picker.delegate.matches.matches {
3449        match &m {
3450            Match::History {
3451                path: history_path,
3452                panel_match: path_match,
3453            } => {
3454                if let Some(path_match) = path_match.as_ref() {
3455                    search_entries
3456                        .history
3457                        .push(path_match.0.path_prefix.join(&path_match.0.path));
3458                } else {
3459                    // This occurs when the query is empty and we show history matches
3460                    // that are outside the project.
3461                    panic!("currently not exercised in tests");
3462                }
3463                search_entries
3464                    .history_found_paths
3465                    .push(history_path.clone());
3466            }
3467            Match::Search(path_match) => {
3468                search_entries
3469                    .search
3470                    .push(path_match.0.path_prefix.join(&path_match.0.path));
3471                search_entries.search_matches.push(path_match.0.clone());
3472            }
3473            Match::CreateNew(_) => {}
3474        }
3475    }
3476    search_entries
3477}
3478
3479#[track_caller]
3480fn assert_match_selection(
3481    finder: &Picker<FileFinderDelegate>,
3482    expected_selection_index: usize,
3483    expected_file_name: &str,
3484) {
3485    assert_eq!(
3486        finder.delegate.selected_index(),
3487        expected_selection_index,
3488        "Match is not selected"
3489    );
3490    assert_match_at_position(finder, expected_selection_index, expected_file_name);
3491}
3492
3493#[track_caller]
3494fn assert_match_at_position(
3495    finder: &Picker<FileFinderDelegate>,
3496    match_index: usize,
3497    expected_file_name: &str,
3498) {
3499    let match_item = finder
3500        .delegate
3501        .matches
3502        .get(match_index)
3503        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
3504    let match_file_name = match &match_item {
3505        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
3506        Match::Search(path_match) => path_match.0.path.file_name(),
3507        Match::CreateNew(project_path) => project_path.path.file_name(),
3508    }
3509    .unwrap();
3510    assert_eq!(match_file_name, expected_file_name);
3511}
3512
3513#[gpui::test]
3514async fn test_filename_precedence(cx: &mut TestAppContext) {
3515    let app_state = init_test(cx);
3516
3517    app_state
3518        .fs
3519        .as_fake()
3520        .insert_tree(
3521            path!("/src"),
3522            json!({
3523                "layout": {
3524                    "app.css": "",
3525                    "app.d.ts": "",
3526                    "app.html": "",
3527                    "+page.svelte": "",
3528                },
3529                "routes": {
3530                    "+layout.svelte": "",
3531                }
3532            }),
3533        )
3534        .await;
3535
3536    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3537    let (picker, _, cx) = build_find_picker(project, cx);
3538
3539    cx.simulate_input("layout");
3540
3541    picker.update(cx, |finder, _| {
3542        let search_matches = collect_search_matches(finder).search_paths_only();
3543
3544        assert_eq!(
3545            search_matches,
3546            vec![
3547                rel_path("routes/+layout.svelte").into(),
3548                rel_path("layout/app.css").into(),
3549                rel_path("layout/app.d.ts").into(),
3550                rel_path("layout/app.html").into(),
3551                rel_path("layout/+page.svelte").into(),
3552            ],
3553            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
3554        );
3555    });
3556}
3557
3558#[gpui::test]
3559async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
3560    let app_state = init_test(cx);
3561    app_state
3562        .fs
3563        .as_fake()
3564        .insert_tree(
3565            path!("/root"),
3566            json!({
3567                "a": {
3568                    "file1.txt": "",
3569                    "b": {
3570                        "file2.txt": "",
3571                    },
3572                }
3573            }),
3574        )
3575        .await;
3576
3577    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3578
3579    let (picker, workspace, cx) = build_find_picker(project, cx);
3580
3581    let matching_abs_path = "/file1.txt".to_string();
3582    picker
3583        .update_in(cx, |picker, window, cx| {
3584            picker
3585                .delegate
3586                .update_matches(matching_abs_path, window, cx)
3587        })
3588        .await;
3589    picker.update(cx, |picker, _| {
3590        assert_eq!(
3591            collect_search_matches(picker).search_paths_only(),
3592            vec![rel_path("a/file1.txt").into()],
3593            "Relative path starting with slash should match"
3594        )
3595    });
3596    cx.dispatch_action(SelectNext);
3597    cx.dispatch_action(Confirm);
3598    cx.read(|cx| {
3599        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3600        assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
3601    });
3602}
3603
3604#[gpui::test]
3605async fn test_clear_navigation_history(cx: &mut TestAppContext) {
3606    let app_state = init_test(cx);
3607    app_state
3608        .fs
3609        .as_fake()
3610        .insert_tree(
3611            path!("/src"),
3612            json!({
3613                "test": {
3614                    "first.rs": "// First file",
3615                    "second.rs": "// Second file",
3616                    "third.rs": "// Third file",
3617                }
3618            }),
3619        )
3620        .await;
3621
3622    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3623    let (multi_workspace, cx) =
3624        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3625    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3626
3627    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
3628
3629    // Open some files to generate navigation history
3630    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
3631    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3632    let history_before_clear =
3633        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
3634
3635    assert_eq!(
3636        history_before_clear.len(),
3637        2,
3638        "Should have history items before clearing"
3639    );
3640
3641    // Verify that file finder shows history items
3642    let picker = open_file_picker(&workspace, cx);
3643    cx.simulate_input("fir");
3644    picker.update(cx, |finder, _| {
3645        let matches = collect_search_matches(finder);
3646        assert!(
3647            !matches.history.is_empty(),
3648            "File finder should show history items before clearing"
3649        );
3650    });
3651    workspace.update_in(cx, |_, window, cx| {
3652        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3653    });
3654
3655    // Verify navigation state before clear
3656    workspace.update(cx, |workspace, cx| {
3657        let pane = workspace.active_pane();
3658        pane.read(cx).can_navigate_backward()
3659    });
3660
3661    // Clear navigation history
3662    cx.dispatch_action(workspace::ClearNavigationHistory);
3663
3664    // Verify that navigation is disabled immediately after clear
3665    workspace.update(cx, |workspace, cx| {
3666        let pane = workspace.active_pane();
3667        assert!(
3668            !pane.read(cx).can_navigate_backward(),
3669            "Should not be able to navigate backward after clearing history"
3670        );
3671        assert!(
3672            !pane.read(cx).can_navigate_forward(),
3673            "Should not be able to navigate forward after clearing history"
3674        );
3675    });
3676
3677    // Verify that file finder no longer shows history items
3678    let picker = open_file_picker(&workspace, cx);
3679    cx.simulate_input("fir");
3680    picker.update(cx, |finder, _| {
3681        let matches = collect_search_matches(finder);
3682        assert!(
3683            matches.history.is_empty(),
3684            "File finder should not show history items after clearing"
3685        );
3686    });
3687    workspace.update_in(cx, |_, window, cx| {
3688        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3689    });
3690
3691    // Verify history is empty by opening a new file
3692    // (this should not show any previous history)
3693    let history_after_clear =
3694        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3695    assert_eq!(
3696        history_after_clear.len(),
3697        0,
3698        "Should have no history items after clearing"
3699    );
3700}