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