project_panel_tests.rs

   1use super::*;
   2use collections::HashSet;
   3use editor::MultiBufferOffset;
   4use gpui::{Empty, Entity, TestAppContext, VisualTestContext};
   5use menu::Cancel;
   6use pretty_assertions::assert_eq;
   7use project::FakeFs;
   8use serde_json::json;
   9use settings::{ProjectPanelAutoOpenSettings, SettingsStore};
  10use std::path::{Path, PathBuf};
  11use util::{path, paths::PathStyle, rel_path::rel_path};
  12use workspace::{
  13    AppState, ItemHandle, MultiWorkspace, Pane, Workspace,
  14    item::{Item, ProjectItem},
  15    register_project_item,
  16};
  17
  18#[gpui::test]
  19async fn test_visible_list(cx: &mut gpui::TestAppContext) {
  20    init_test(cx);
  21
  22    let fs = FakeFs::new(cx.executor());
  23    fs.insert_tree(
  24        "/root1",
  25        json!({
  26            ".dockerignore": "",
  27            ".git": {
  28                "HEAD": "",
  29            },
  30            "a": {
  31                "0": { "q": "", "r": "", "s": "" },
  32                "1": { "t": "", "u": "" },
  33                "2": { "v": "", "w": "", "x": "", "y": "" },
  34            },
  35            "b": {
  36                "3": { "Q": "" },
  37                "4": { "R": "", "S": "", "T": "", "U": "" },
  38            },
  39            "C": {
  40                "5": {},
  41                "6": { "V": "", "W": "" },
  42                "7": { "X": "" },
  43                "8": { "Y": {}, "Z": "" }
  44            }
  45        }),
  46    )
  47    .await;
  48    fs.insert_tree(
  49        "/root2",
  50        json!({
  51            "d": {
  52                "9": ""
  53            },
  54            "e": {}
  55        }),
  56    )
  57    .await;
  58
  59    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  60    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  61    let workspace = window
  62        .read_with(cx, |mw, _| mw.workspace().clone())
  63        .unwrap();
  64    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  65    let panel = workspace.update_in(cx, ProjectPanel::new);
  66    cx.run_until_parked();
  67    assert_eq!(
  68        visible_entries_as_strings(&panel, 0..50, cx),
  69        &[
  70            "v root1",
  71            "    > .git",
  72            "    > a",
  73            "    > b",
  74            "    > C",
  75            "      .dockerignore",
  76            "v root2",
  77            "    > d",
  78            "    > e",
  79        ]
  80    );
  81
  82    toggle_expand_dir(&panel, "root1/b", cx);
  83    assert_eq!(
  84        visible_entries_as_strings(&panel, 0..50, cx),
  85        &[
  86            "v root1",
  87            "    > .git",
  88            "    > a",
  89            "    v b  <== selected",
  90            "        > 3",
  91            "        > 4",
  92            "    > C",
  93            "      .dockerignore",
  94            "v root2",
  95            "    > d",
  96            "    > e",
  97        ]
  98    );
  99
 100    assert_eq!(
 101        visible_entries_as_strings(&panel, 6..9, cx),
 102        &[
 103            //
 104            "    > C",
 105            "      .dockerignore",
 106            "v root2",
 107        ]
 108    );
 109}
 110
 111#[gpui::test]
 112async fn test_opening_file(cx: &mut gpui::TestAppContext) {
 113    init_test_with_editor(cx);
 114
 115    let fs = FakeFs::new(cx.executor());
 116    fs.insert_tree(
 117        path!("/src"),
 118        json!({
 119            "test": {
 120                "first.rs": "// First Rust file",
 121                "second.rs": "// Second Rust file",
 122                "third.rs": "// Third Rust file",
 123            }
 124        }),
 125    )
 126    .await;
 127
 128    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
 129    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 130    let workspace = window
 131        .read_with(cx, |mw, _| mw.workspace().clone())
 132        .unwrap();
 133    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 134    let panel = workspace.update_in(cx, ProjectPanel::new);
 135    cx.run_until_parked();
 136
 137    toggle_expand_dir(&panel, "src/test", cx);
 138    select_path(&panel, "src/test/first.rs", cx);
 139    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 140    cx.executor().run_until_parked();
 141    assert_eq!(
 142        visible_entries_as_strings(&panel, 0..10, cx),
 143        &[
 144            "v src",
 145            "    v test",
 146            "          first.rs  <== selected  <== marked",
 147            "          second.rs",
 148            "          third.rs"
 149        ]
 150    );
 151    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
 152
 153    select_path(&panel, "src/test/second.rs", cx);
 154    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 155    cx.executor().run_until_parked();
 156    assert_eq!(
 157        visible_entries_as_strings(&panel, 0..10, cx),
 158        &[
 159            "v src",
 160            "    v test",
 161            "          first.rs",
 162            "          second.rs  <== selected  <== marked",
 163            "          third.rs"
 164        ]
 165    );
 166    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
 167}
 168
 169#[gpui::test]
 170async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
 171    init_test(cx);
 172    cx.update(|cx| {
 173        cx.update_global::<SettingsStore, _>(|store, cx| {
 174            store.update_user_settings(cx, |settings| {
 175                settings.project.worktree.file_scan_exclusions =
 176                    Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
 177            });
 178        });
 179    });
 180
 181    let fs = FakeFs::new(cx.background_executor.clone());
 182    fs.insert_tree(
 183        "/root1",
 184        json!({
 185            ".dockerignore": "",
 186            ".git": {
 187                "HEAD": "",
 188            },
 189            "a": {
 190                "0": { "q": "", "r": "", "s": "" },
 191                "1": { "t": "", "u": "" },
 192                "2": { "v": "", "w": "", "x": "", "y": "" },
 193            },
 194            "b": {
 195                "3": { "Q": "" },
 196                "4": { "R": "", "S": "", "T": "", "U": "" },
 197            },
 198            "C": {
 199                "5": {},
 200                "6": { "V": "", "W": "" },
 201                "7": { "X": "" },
 202                "8": { "Y": {}, "Z": "" }
 203            }
 204        }),
 205    )
 206    .await;
 207    fs.insert_tree(
 208        "/root2",
 209        json!({
 210            "d": {
 211                "4": ""
 212            },
 213            "e": {}
 214        }),
 215    )
 216    .await;
 217
 218    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 219    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 220    let workspace = window
 221        .read_with(cx, |mw, _| mw.workspace().clone())
 222        .unwrap();
 223    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 224    let panel = workspace.update_in(cx, ProjectPanel::new);
 225    cx.run_until_parked();
 226    assert_eq!(
 227        visible_entries_as_strings(&panel, 0..50, cx),
 228        &[
 229            "v root1",
 230            "    > a",
 231            "    > b",
 232            "    > C",
 233            "      .dockerignore",
 234            "v root2",
 235            "    > d",
 236            "    > e",
 237        ]
 238    );
 239
 240    toggle_expand_dir(&panel, "root1/b", cx);
 241    assert_eq!(
 242        visible_entries_as_strings(&panel, 0..50, cx),
 243        &[
 244            "v root1",
 245            "    > a",
 246            "    v b  <== selected",
 247            "        > 3",
 248            "    > C",
 249            "      .dockerignore",
 250            "v root2",
 251            "    > d",
 252            "    > e",
 253        ]
 254    );
 255
 256    toggle_expand_dir(&panel, "root2/d", cx);
 257    assert_eq!(
 258        visible_entries_as_strings(&panel, 0..50, cx),
 259        &[
 260            "v root1",
 261            "    > a",
 262            "    v b",
 263            "        > 3",
 264            "    > C",
 265            "      .dockerignore",
 266            "v root2",
 267            "    v d  <== selected",
 268            "    > e",
 269        ]
 270    );
 271
 272    toggle_expand_dir(&panel, "root2/e", cx);
 273    assert_eq!(
 274        visible_entries_as_strings(&panel, 0..50, cx),
 275        &[
 276            "v root1",
 277            "    > a",
 278            "    v b",
 279            "        > 3",
 280            "    > C",
 281            "      .dockerignore",
 282            "v root2",
 283            "    v d",
 284            "    v e  <== selected",
 285        ]
 286    );
 287}
 288
 289#[gpui::test]
 290async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
 291    init_test(cx);
 292
 293    let fs = FakeFs::new(cx.executor());
 294    fs.insert_tree(
 295        path!("/root1"),
 296        json!({
 297            "dir_1": {
 298                "nested_dir_1": {
 299                    "nested_dir_2": {
 300                        "nested_dir_3": {
 301                            "file_a.java": "// File contents",
 302                            "file_b.java": "// File contents",
 303                            "file_c.java": "// File contents",
 304                            "nested_dir_4": {
 305                                "nested_dir_5": {
 306                                    "file_d.java": "// File contents",
 307                                }
 308                            }
 309                        }
 310                    }
 311                }
 312            }
 313        }),
 314    )
 315    .await;
 316    fs.insert_tree(
 317        path!("/root2"),
 318        json!({
 319            "dir_2": {
 320                "file_1.java": "// File contents",
 321            }
 322        }),
 323    )
 324    .await;
 325
 326    // Test 1: Multiple worktrees with auto_fold_dirs = true
 327    let project = Project::test(
 328        fs.clone(),
 329        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 330        cx,
 331    )
 332    .await;
 333    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 334    let workspace = window
 335        .read_with(cx, |mw, _| mw.workspace().clone())
 336        .unwrap();
 337    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 338    cx.update(|_, cx| {
 339        let settings = *ProjectPanelSettings::get_global(cx);
 340        ProjectPanelSettings::override_global(
 341            ProjectPanelSettings {
 342                auto_fold_dirs: true,
 343                sort_mode: settings::ProjectPanelSortMode::DirectoriesFirst,
 344                ..settings
 345            },
 346            cx,
 347        );
 348    });
 349    let panel = workspace.update_in(cx, ProjectPanel::new);
 350    cx.run_until_parked();
 351    assert_eq!(
 352        visible_entries_as_strings(&panel, 0..10, cx),
 353        &[
 354            "v root1",
 355            "    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 356            "v root2",
 357            "    > dir_2",
 358        ]
 359    );
 360
 361    toggle_expand_dir(
 362        &panel,
 363        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 364        cx,
 365    );
 366    assert_eq!(
 367        visible_entries_as_strings(&panel, 0..10, cx),
 368        &[
 369            "v root1",
 370            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
 371            "        > nested_dir_4/nested_dir_5",
 372            "          file_a.java",
 373            "          file_b.java",
 374            "          file_c.java",
 375            "v root2",
 376            "    > dir_2",
 377        ]
 378    );
 379
 380    toggle_expand_dir(
 381        &panel,
 382        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
 383        cx,
 384    );
 385    assert_eq!(
 386        visible_entries_as_strings(&panel, 0..10, cx),
 387        &[
 388            "v root1",
 389            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 390            "        v nested_dir_4/nested_dir_5  <== selected",
 391            "              file_d.java",
 392            "          file_a.java",
 393            "          file_b.java",
 394            "          file_c.java",
 395            "v root2",
 396            "    > dir_2",
 397        ]
 398    );
 399    toggle_expand_dir(&panel, "root2/dir_2", cx);
 400    assert_eq!(
 401        visible_entries_as_strings(&panel, 0..10, cx),
 402        &[
 403            "v root1",
 404            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 405            "        v nested_dir_4/nested_dir_5",
 406            "              file_d.java",
 407            "          file_a.java",
 408            "          file_b.java",
 409            "          file_c.java",
 410            "v root2",
 411            "    v dir_2  <== selected",
 412            "          file_1.java",
 413        ]
 414    );
 415
 416    // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
 417    {
 418        let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
 419        let window =
 420            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 421        let workspace = window
 422            .read_with(cx, |mw, _| mw.workspace().clone())
 423            .unwrap();
 424        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 425        cx.update(|_, cx| {
 426            let settings = *ProjectPanelSettings::get_global(cx);
 427            ProjectPanelSettings::override_global(
 428                ProjectPanelSettings {
 429                    auto_fold_dirs: true,
 430                    hide_root: true,
 431                    ..settings
 432                },
 433                cx,
 434            );
 435        });
 436        let panel = workspace.update_in(cx, ProjectPanel::new);
 437        cx.run_until_parked();
 438        assert_eq!(
 439            visible_entries_as_strings(&panel, 0..10, cx),
 440            &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"],
 441            "Single worktree with hide_root=true should hide root and show auto-folded paths"
 442        );
 443
 444        toggle_expand_dir(
 445            &panel,
 446            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 447            cx,
 448        );
 449        assert_eq!(
 450            visible_entries_as_strings(&panel, 0..10, cx),
 451            &[
 452                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
 453                "    > nested_dir_4/nested_dir_5",
 454                "      file_a.java",
 455                "      file_b.java",
 456                "      file_c.java",
 457            ],
 458            "Expanded auto-folded path with hidden root should show contents without root prefix"
 459        );
 460
 461        toggle_expand_dir(
 462            &panel,
 463            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
 464            cx,
 465        );
 466        assert_eq!(
 467            visible_entries_as_strings(&panel, 0..10, cx),
 468            &[
 469                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 470                "    v nested_dir_4/nested_dir_5  <== selected",
 471                "          file_d.java",
 472                "      file_a.java",
 473                "      file_b.java",
 474                "      file_c.java",
 475            ],
 476            "Nested expansion with hidden root should maintain proper indentation"
 477        );
 478    }
 479}
 480
 481#[gpui::test(iterations = 30)]
 482async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 483    init_test(cx);
 484
 485    let fs = FakeFs::new(cx.executor());
 486    fs.insert_tree(
 487        "/root1",
 488        json!({
 489            ".dockerignore": "",
 490            ".git": {
 491                "HEAD": "",
 492            },
 493            "a": {
 494                "0": { "q": "", "r": "", "s": "" },
 495                "1": { "t": "", "u": "" },
 496                "2": { "v": "", "w": "", "x": "", "y": "" },
 497            },
 498            "b": {
 499                "3": { "Q": "" },
 500                "4": { "R": "", "S": "", "T": "", "U": "" },
 501            },
 502            "C": {
 503                "5": {},
 504                "6": { "V": "", "W": "" },
 505                "7": { "X": "" },
 506                "8": { "Y": {}, "Z": "" }
 507            }
 508        }),
 509    )
 510    .await;
 511    fs.insert_tree(
 512        "/root2",
 513        json!({
 514            "d": {
 515                "9": ""
 516            },
 517            "e": {}
 518        }),
 519    )
 520    .await;
 521
 522    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 523    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 524    let workspace = window
 525        .read_with(cx, |mw, _| mw.workspace().clone())
 526        .unwrap();
 527    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 528    let panel = workspace.update_in(cx, |workspace, window, cx| {
 529        let panel = ProjectPanel::new(workspace, window, cx);
 530        workspace.add_panel(panel.clone(), window, cx);
 531        panel
 532    });
 533    cx.run_until_parked();
 534
 535    select_path(&panel, "root1", cx);
 536    assert_eq!(
 537        visible_entries_as_strings(&panel, 0..10, cx),
 538        &[
 539            "v root1  <== selected",
 540            "    > .git",
 541            "    > a",
 542            "    > b",
 543            "    > C",
 544            "      .dockerignore",
 545            "v root2",
 546            "    > d",
 547            "    > e",
 548        ]
 549    );
 550
 551    // Add a file with the root folder selected. The filename editor is placed
 552    // before the first file in the root folder.
 553    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 554    cx.run_until_parked();
 555    panel.update_in(cx, |panel, window, cx| {
 556        assert!(panel.filename_editor.read(cx).is_focused(window));
 557    });
 558    assert_eq!(
 559        visible_entries_as_strings(&panel, 0..10, cx),
 560        &[
 561            "v root1",
 562            "    > .git",
 563            "    > a",
 564            "    > b",
 565            "    > C",
 566            "      [EDITOR: '']  <== selected",
 567            "      .dockerignore",
 568            "v root2",
 569            "    > d",
 570            "    > e",
 571        ]
 572    );
 573
 574    let confirm = panel.update_in(cx, |panel, window, cx| {
 575        panel.filename_editor.update(cx, |editor, cx| {
 576            editor.set_text("the-new-filename", window, cx)
 577        });
 578        panel.confirm_edit(true, window, cx).unwrap()
 579    });
 580    assert_eq!(
 581        visible_entries_as_strings(&panel, 0..10, cx),
 582        &[
 583            "v root1",
 584            "    > .git",
 585            "    > a",
 586            "    > b",
 587            "    > C",
 588            "      [PROCESSING: 'the-new-filename']  <== selected",
 589            "      .dockerignore",
 590            "v root2",
 591            "    > d",
 592            "    > e",
 593        ]
 594    );
 595
 596    confirm.await.unwrap();
 597    cx.run_until_parked();
 598    assert_eq!(
 599        visible_entries_as_strings(&panel, 0..10, cx),
 600        &[
 601            "v root1",
 602            "    > .git",
 603            "    > a",
 604            "    > b",
 605            "    > C",
 606            "      .dockerignore",
 607            "      the-new-filename  <== selected  <== marked",
 608            "v root2",
 609            "    > d",
 610            "    > e",
 611        ]
 612    );
 613
 614    select_path(&panel, "root1/b", cx);
 615    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 616    cx.run_until_parked();
 617    assert_eq!(
 618        visible_entries_as_strings(&panel, 0..10, cx),
 619        &[
 620            "v root1",
 621            "    > .git",
 622            "    > a",
 623            "    v b",
 624            "        > 3",
 625            "        > 4",
 626            "          [EDITOR: '']  <== selected",
 627            "    > C",
 628            "      .dockerignore",
 629            "      the-new-filename",
 630        ]
 631    );
 632
 633    panel
 634        .update_in(cx, |panel, window, cx| {
 635            panel.filename_editor.update(cx, |editor, cx| {
 636                editor.set_text("another-filename.txt", window, cx)
 637            });
 638            panel.confirm_edit(true, window, cx).unwrap()
 639        })
 640        .await
 641        .unwrap();
 642    cx.run_until_parked();
 643    assert_eq!(
 644        visible_entries_as_strings(&panel, 0..10, cx),
 645        &[
 646            "v root1",
 647            "    > .git",
 648            "    > a",
 649            "    v b",
 650            "        > 3",
 651            "        > 4",
 652            "          another-filename.txt  <== selected  <== marked",
 653            "    > C",
 654            "      .dockerignore",
 655            "      the-new-filename",
 656        ]
 657    );
 658
 659    select_path(&panel, "root1/b/another-filename.txt", cx);
 660    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 661    assert_eq!(
 662        visible_entries_as_strings(&panel, 0..10, cx),
 663        &[
 664            "v root1",
 665            "    > .git",
 666            "    > a",
 667            "    v b",
 668            "        > 3",
 669            "        > 4",
 670            "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
 671            "    > C",
 672            "      .dockerignore",
 673            "      the-new-filename",
 674        ]
 675    );
 676
 677    let confirm = panel.update_in(cx, |panel, window, cx| {
 678        panel.filename_editor.update(cx, |editor, cx| {
 679            let file_name_selections = editor
 680                .selections
 681                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
 682            assert_eq!(
 683                file_name_selections.len(),
 684                1,
 685                "File editing should have a single selection, but got: {file_name_selections:?}"
 686            );
 687            let file_name_selection = &file_name_selections[0];
 688            assert_eq!(
 689                file_name_selection.start,
 690                MultiBufferOffset(0),
 691                "Should select the file name from the start"
 692            );
 693            assert_eq!(
 694                file_name_selection.end,
 695                MultiBufferOffset("another-filename".len()),
 696                "Should not select file extension"
 697            );
 698
 699            editor.set_text("a-different-filename.tar.gz", window, cx)
 700        });
 701        panel.confirm_edit(true, window, cx).unwrap()
 702    });
 703    assert_eq!(
 704        visible_entries_as_strings(&panel, 0..10, cx),
 705        &[
 706            "v root1",
 707            "    > .git",
 708            "    > a",
 709            "    v b",
 710            "        > 3",
 711            "        > 4",
 712            "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
 713            "    > C",
 714            "      .dockerignore",
 715            "      the-new-filename",
 716        ]
 717    );
 718
 719    confirm.await.unwrap();
 720    cx.run_until_parked();
 721    assert_eq!(
 722        visible_entries_as_strings(&panel, 0..10, cx),
 723        &[
 724            "v root1",
 725            "    > .git",
 726            "    > a",
 727            "    v b",
 728            "        > 3",
 729            "        > 4",
 730            "          a-different-filename.tar.gz  <== selected",
 731            "    > C",
 732            "      .dockerignore",
 733            "      the-new-filename",
 734        ]
 735    );
 736
 737    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 738    assert_eq!(
 739        visible_entries_as_strings(&panel, 0..10, cx),
 740        &[
 741            "v root1",
 742            "    > .git",
 743            "    > a",
 744            "    v b",
 745            "        > 3",
 746            "        > 4",
 747            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
 748            "    > C",
 749            "      .dockerignore",
 750            "      the-new-filename",
 751        ]
 752    );
 753
 754    panel.update_in(cx, |panel, window, cx| {
 755            panel.filename_editor.update(cx, |editor, cx| {
 756                let file_name_selections = editor.selections.all::<MultiBufferOffset>(&editor.display_snapshot(cx));
 757                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
 758                let file_name_selection = &file_name_selections[0];
 759                assert_eq!(file_name_selection.start, MultiBufferOffset(0), "Should select the file name from the start");
 760                assert_eq!(file_name_selection.end, MultiBufferOffset("a-different-filename.tar".len()), "Should not select file extension, but still may select anything up to the last dot..");
 761
 762            });
 763            panel.cancel(&menu::Cancel, window, cx)
 764        });
 765    cx.run_until_parked();
 766    panel.update_in(cx, |panel, window, cx| {
 767        panel.new_directory(&NewDirectory, window, cx)
 768    });
 769    cx.run_until_parked();
 770    assert_eq!(
 771        visible_entries_as_strings(&panel, 0..10, cx),
 772        &[
 773            "v root1",
 774            "    > .git",
 775            "    > a",
 776            "    v b",
 777            "        > [EDITOR: '']  <== selected",
 778            "        > 3",
 779            "        > 4",
 780            "          a-different-filename.tar.gz",
 781            "    > C",
 782            "      .dockerignore",
 783        ]
 784    );
 785
 786    let confirm = panel.update_in(cx, |panel, window, cx| {
 787        panel
 788            .filename_editor
 789            .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
 790        panel.confirm_edit(true, window, cx).unwrap()
 791    });
 792    panel.update_in(cx, |panel, window, cx| {
 793        panel.select_next(&Default::default(), window, cx)
 794    });
 795    assert_eq!(
 796        visible_entries_as_strings(&panel, 0..10, cx),
 797        &[
 798            "v root1",
 799            "    > .git",
 800            "    > a",
 801            "    v b",
 802            "        > [PROCESSING: 'new-dir']",
 803            "        > 3  <== selected",
 804            "        > 4",
 805            "          a-different-filename.tar.gz",
 806            "    > C",
 807            "      .dockerignore",
 808        ]
 809    );
 810
 811    confirm.await.unwrap();
 812    cx.run_until_parked();
 813    assert_eq!(
 814        visible_entries_as_strings(&panel, 0..10, cx),
 815        &[
 816            "v root1",
 817            "    > .git",
 818            "    > a",
 819            "    v b",
 820            "        > 3  <== selected",
 821            "        > 4",
 822            "        > new-dir",
 823            "          a-different-filename.tar.gz",
 824            "    > C",
 825            "      .dockerignore",
 826        ]
 827    );
 828
 829    panel.update_in(cx, |panel, window, cx| {
 830        panel.rename(&Default::default(), window, cx)
 831    });
 832    cx.run_until_parked();
 833    assert_eq!(
 834        visible_entries_as_strings(&panel, 0..10, cx),
 835        &[
 836            "v root1",
 837            "    > .git",
 838            "    > a",
 839            "    v b",
 840            "        > [EDITOR: '3']  <== selected",
 841            "        > 4",
 842            "        > new-dir",
 843            "          a-different-filename.tar.gz",
 844            "    > C",
 845            "      .dockerignore",
 846        ]
 847    );
 848
 849    // Dismiss the rename editor when it loses focus.
 850    workspace.update_in(cx, |_, window, _| window.blur());
 851    assert_eq!(
 852        visible_entries_as_strings(&panel, 0..10, cx),
 853        &[
 854            "v root1",
 855            "    > .git",
 856            "    > a",
 857            "    v b",
 858            "        > 3  <== selected",
 859            "        > 4",
 860            "        > new-dir",
 861            "          a-different-filename.tar.gz",
 862            "    > C",
 863            "      .dockerignore",
 864        ]
 865    );
 866
 867    // Test empty filename and filename with only whitespace
 868    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 869    cx.run_until_parked();
 870    assert_eq!(
 871        visible_entries_as_strings(&panel, 0..10, cx),
 872        &[
 873            "v root1",
 874            "    > .git",
 875            "    > a",
 876            "    v b",
 877            "        v 3",
 878            "              [EDITOR: '']  <== selected",
 879            "              Q",
 880            "        > 4",
 881            "        > new-dir",
 882            "          a-different-filename.tar.gz",
 883        ]
 884    );
 885    panel.update_in(cx, |panel, window, cx| {
 886        panel.filename_editor.update(cx, |editor, cx| {
 887            editor.set_text("", window, cx);
 888        });
 889        assert!(panel.confirm_edit(true, window, cx).is_none());
 890        panel.filename_editor.update(cx, |editor, cx| {
 891            editor.set_text("   ", window, cx);
 892        });
 893        assert!(panel.confirm_edit(true, window, cx).is_none());
 894        panel.cancel(&menu::Cancel, window, cx);
 895    });
 896    cx.run_until_parked();
 897    assert_eq!(
 898        visible_entries_as_strings(&panel, 0..10, cx),
 899        &[
 900            "v root1",
 901            "    > .git",
 902            "    > a",
 903            "    v b",
 904            "        v 3  <== selected",
 905            "              Q",
 906            "        > 4",
 907            "        > new-dir",
 908            "          a-different-filename.tar.gz",
 909            "    > C",
 910        ]
 911    );
 912}
 913
 914#[gpui::test(iterations = 10)]
 915async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
 916    init_test(cx);
 917
 918    let fs = FakeFs::new(cx.executor());
 919    fs.insert_tree(
 920        "/root1",
 921        json!({
 922            ".dockerignore": "",
 923            ".git": {
 924                "HEAD": "",
 925            },
 926            "a": {
 927                "0": { "q": "", "r": "", "s": "" },
 928                "1": { "t": "", "u": "" },
 929                "2": { "v": "", "w": "", "x": "", "y": "" },
 930            },
 931            "b": {
 932                "3": { "Q": "" },
 933                "4": { "R": "", "S": "", "T": "", "U": "" },
 934            },
 935            "C": {
 936                "5": {},
 937                "6": { "V": "", "W": "" },
 938                "7": { "X": "" },
 939                "8": { "Y": {}, "Z": "" }
 940            }
 941        }),
 942    )
 943    .await;
 944    fs.insert_tree(
 945        "/root2",
 946        json!({
 947            "d": {
 948                "9": ""
 949            },
 950            "e": {}
 951        }),
 952    )
 953    .await;
 954
 955    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 956    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 957    let workspace = window
 958        .read_with(cx, |mw, _| mw.workspace().clone())
 959        .unwrap();
 960    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 961    let panel = workspace.update_in(cx, |workspace, window, cx| {
 962        let panel = ProjectPanel::new(workspace, window, cx);
 963        workspace.add_panel(panel.clone(), window, cx);
 964        panel
 965    });
 966    cx.run_until_parked();
 967
 968    select_path(&panel, "root1", cx);
 969    assert_eq!(
 970        visible_entries_as_strings(&panel, 0..10, cx),
 971        &[
 972            "v root1  <== selected",
 973            "    > .git",
 974            "    > a",
 975            "    > b",
 976            "    > C",
 977            "      .dockerignore",
 978            "v root2",
 979            "    > d",
 980            "    > e",
 981        ]
 982    );
 983
 984    // Add a file with the root folder selected. The filename editor is placed
 985    // before the first file in the root folder.
 986    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 987    cx.run_until_parked();
 988    panel.update_in(cx, |panel, window, cx| {
 989        assert!(panel.filename_editor.read(cx).is_focused(window));
 990    });
 991    cx.run_until_parked();
 992    assert_eq!(
 993        visible_entries_as_strings(&panel, 0..10, cx),
 994        &[
 995            "v root1",
 996            "    > .git",
 997            "    > a",
 998            "    > b",
 999            "    > C",
1000            "      [EDITOR: '']  <== selected",
1001            "      .dockerignore",
1002            "v root2",
1003            "    > d",
1004            "    > e",
1005        ]
1006    );
1007
1008    let confirm = panel.update_in(cx, |panel, window, cx| {
1009        panel.filename_editor.update(cx, |editor, cx| {
1010            editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
1011        });
1012        panel.confirm_edit(true, window, cx).unwrap()
1013    });
1014
1015    assert_eq!(
1016        visible_entries_as_strings(&panel, 0..10, cx),
1017        &[
1018            "v root1",
1019            "    > .git",
1020            "    > a",
1021            "    > b",
1022            "    > C",
1023            "      [PROCESSING: 'bdir1/dir2/the-new-filename']  <== selected",
1024            "      .dockerignore",
1025            "v root2",
1026            "    > d",
1027            "    > e",
1028        ]
1029    );
1030
1031    confirm.await.unwrap();
1032    cx.run_until_parked();
1033    assert_eq!(
1034        visible_entries_as_strings(&panel, 0..13, cx),
1035        &[
1036            "v root1",
1037            "    > .git",
1038            "    > a",
1039            "    > b",
1040            "    v bdir1",
1041            "        v dir2",
1042            "              the-new-filename  <== selected  <== marked",
1043            "    > C",
1044            "      .dockerignore",
1045            "v root2",
1046            "    > d",
1047            "    > e",
1048        ]
1049    );
1050}
1051
1052#[gpui::test]
1053async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
1054    init_test(cx);
1055
1056    let fs = FakeFs::new(cx.executor());
1057    fs.insert_tree(
1058        path!("/root1"),
1059        json!({
1060            ".dockerignore": "",
1061            ".git": {
1062                "HEAD": "",
1063            },
1064        }),
1065    )
1066    .await;
1067
1068    let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
1069    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1070    let workspace = window
1071        .read_with(cx, |mw, _| mw.workspace().clone())
1072        .unwrap();
1073    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1074    let panel = workspace.update_in(cx, |workspace, window, cx| {
1075        let panel = ProjectPanel::new(workspace, window, cx);
1076        workspace.add_panel(panel.clone(), window, cx);
1077        panel
1078    });
1079    cx.run_until_parked();
1080
1081    select_path(&panel, "root1", cx);
1082    assert_eq!(
1083        visible_entries_as_strings(&panel, 0..10, cx),
1084        &["v root1  <== selected", "    > .git", "      .dockerignore",]
1085    );
1086
1087    // Add a file with the root folder selected. The filename editor is placed
1088    // before the first file in the root folder.
1089    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1090    cx.run_until_parked();
1091    panel.update_in(cx, |panel, window, cx| {
1092        assert!(panel.filename_editor.read(cx).is_focused(window));
1093    });
1094    assert_eq!(
1095        visible_entries_as_strings(&panel, 0..10, cx),
1096        &[
1097            "v root1",
1098            "    > .git",
1099            "      [EDITOR: '']  <== selected",
1100            "      .dockerignore",
1101        ]
1102    );
1103
1104    let confirm = panel.update_in(cx, |panel, window, cx| {
1105        // If we want to create a subdirectory, there should be no prefix slash.
1106        panel
1107            .filename_editor
1108            .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
1109        panel.confirm_edit(true, window, cx).unwrap()
1110    });
1111
1112    assert_eq!(
1113        visible_entries_as_strings(&panel, 0..10, cx),
1114        &[
1115            "v root1",
1116            "    > .git",
1117            "      [PROCESSING: 'new_dir']  <== selected",
1118            "      .dockerignore",
1119        ]
1120    );
1121
1122    confirm.await.unwrap();
1123    cx.run_until_parked();
1124    assert_eq!(
1125        visible_entries_as_strings(&panel, 0..10, cx),
1126        &[
1127            "v root1",
1128            "    > .git",
1129            "    v new_dir  <== selected",
1130            "      .dockerignore",
1131        ]
1132    );
1133
1134    // Test filename with whitespace
1135    select_path(&panel, "root1", cx);
1136    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1137    let confirm = panel.update_in(cx, |panel, window, cx| {
1138        // If we want to create a subdirectory, there should be no prefix slash.
1139        panel
1140            .filename_editor
1141            .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
1142        panel.confirm_edit(true, window, cx).unwrap()
1143    });
1144    confirm.await.unwrap();
1145    cx.run_until_parked();
1146    assert_eq!(
1147        visible_entries_as_strings(&panel, 0..10, cx),
1148        &[
1149            "v root1",
1150            "    > .git",
1151            "    v new dir 2  <== selected",
1152            "    v new_dir",
1153            "      .dockerignore",
1154        ]
1155    );
1156
1157    // Test filename ends with "\"
1158    #[cfg(target_os = "windows")]
1159    {
1160        select_path(&panel, "root1", cx);
1161        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1162        let confirm = panel.update_in(cx, |panel, window, cx| {
1163            // If we want to create a subdirectory, there should be no prefix slash.
1164            panel
1165                .filename_editor
1166                .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
1167            panel.confirm_edit(true, window, cx).unwrap()
1168        });
1169        confirm.await.unwrap();
1170        cx.run_until_parked();
1171        assert_eq!(
1172            visible_entries_as_strings(&panel, 0..10, cx),
1173            &[
1174                "v root1",
1175                "    > .git",
1176                "    v new dir 2",
1177                "    v new_dir",
1178                "    v new_dir_3  <== selected",
1179                "      .dockerignore",
1180            ]
1181        );
1182    }
1183}
1184
1185#[gpui::test]
1186async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1187    init_test(cx);
1188
1189    let fs = FakeFs::new(cx.executor());
1190    fs.insert_tree(
1191        "/root1",
1192        json!({
1193            "one.two.txt": "",
1194            "one.txt": ""
1195        }),
1196    )
1197    .await;
1198
1199    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1200    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1201    let workspace = window
1202        .read_with(cx, |mw, _| mw.workspace().clone())
1203        .unwrap();
1204    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1205    let panel = workspace.update_in(cx, ProjectPanel::new);
1206    cx.run_until_parked();
1207
1208    panel.update_in(cx, |panel, window, cx| {
1209        panel.select_next(&Default::default(), window, cx);
1210        panel.select_next(&Default::default(), window, cx);
1211    });
1212
1213    assert_eq!(
1214        visible_entries_as_strings(&panel, 0..50, cx),
1215        &[
1216            //
1217            "v root1",
1218            "      one.txt  <== selected",
1219            "      one.two.txt",
1220        ]
1221    );
1222
1223    // Regression test - file name is created correctly when
1224    // the copied file's name contains multiple dots.
1225    panel.update_in(cx, |panel, window, cx| {
1226        panel.copy(&Default::default(), window, cx);
1227        panel.paste(&Default::default(), window, cx);
1228    });
1229    cx.executor().run_until_parked();
1230    panel.update_in(cx, |panel, window, cx| {
1231        assert!(panel.filename_editor.read(cx).is_focused(window));
1232    });
1233    assert_eq!(
1234        visible_entries_as_strings(&panel, 0..50, cx),
1235        &[
1236            //
1237            "v root1",
1238            "      one.txt",
1239            "      [EDITOR: 'one copy.txt']  <== selected  <== marked",
1240            "      one.two.txt",
1241        ]
1242    );
1243
1244    panel.update_in(cx, |panel, window, cx| {
1245        panel.filename_editor.update(cx, |editor, cx| {
1246            let file_name_selections = editor
1247                .selections
1248                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
1249            assert_eq!(
1250                file_name_selections.len(),
1251                1,
1252                "File editing should have a single selection, but got: {file_name_selections:?}"
1253            );
1254            let file_name_selection = &file_name_selections[0];
1255            assert_eq!(
1256                file_name_selection.start,
1257                MultiBufferOffset("one".len()),
1258                "Should select the file name disambiguation after the original file name"
1259            );
1260            assert_eq!(
1261                file_name_selection.end,
1262                MultiBufferOffset("one copy".len()),
1263                "Should select the file name disambiguation until the extension"
1264            );
1265        });
1266        assert!(panel.confirm_edit(true, window, cx).is_none());
1267    });
1268
1269    panel.update_in(cx, |panel, window, cx| {
1270        panel.paste(&Default::default(), window, cx);
1271    });
1272    cx.executor().run_until_parked();
1273    panel.update_in(cx, |panel, window, cx| {
1274        assert!(panel.filename_editor.read(cx).is_focused(window));
1275    });
1276    assert_eq!(
1277        visible_entries_as_strings(&panel, 0..50, cx),
1278        &[
1279            //
1280            "v root1",
1281            "      one.txt",
1282            "      one copy.txt",
1283            "      [EDITOR: 'one copy 1.txt']  <== selected  <== marked",
1284            "      one.two.txt",
1285        ]
1286    );
1287
1288    panel.update_in(cx, |panel, window, cx| {
1289        assert!(panel.confirm_edit(true, window, cx).is_none())
1290    });
1291}
1292
1293#[gpui::test]
1294async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
1295    init_test(cx);
1296
1297    let fs = FakeFs::new(cx.executor());
1298    fs.insert_tree(
1299        "/root",
1300        json!({
1301            "one.txt": "",
1302            "two.txt": "",
1303            "a": {},
1304            "b": {}
1305        }),
1306    )
1307    .await;
1308
1309    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1310    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1311    let workspace = window
1312        .read_with(cx, |mw, _| mw.workspace().clone())
1313        .unwrap();
1314    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1315    let panel = workspace.update_in(cx, ProjectPanel::new);
1316    cx.run_until_parked();
1317
1318    select_path_with_mark(&panel, "root/one.txt", cx);
1319    select_path_with_mark(&panel, "root/two.txt", cx);
1320
1321    assert_eq!(
1322        visible_entries_as_strings(&panel, 0..50, cx),
1323        &[
1324            "v root",
1325            "    > a",
1326            "    > b",
1327            "      one.txt  <== marked",
1328            "      two.txt  <== selected  <== marked",
1329        ]
1330    );
1331
1332    panel.update_in(cx, |panel, window, cx| {
1333        panel.cut(&Default::default(), window, cx);
1334    });
1335
1336    select_path(&panel, "root/a", cx);
1337
1338    panel.update_in(cx, |panel, window, cx| {
1339        panel.paste(&Default::default(), window, cx);
1340        panel.update_visible_entries(None, false, false, window, cx);
1341    });
1342    cx.executor().run_until_parked();
1343
1344    assert_eq!(
1345        visible_entries_as_strings(&panel, 0..50, cx),
1346        &[
1347            "v root",
1348            "    v a",
1349            "          one.txt  <== marked",
1350            "          two.txt  <== selected  <== marked",
1351            "    > b",
1352        ],
1353        "Cut entries should be moved on first paste."
1354    );
1355
1356    panel.update_in(cx, |panel, window, cx| {
1357        panel.cancel(&menu::Cancel {}, window, cx)
1358    });
1359    cx.executor().run_until_parked();
1360
1361    select_path(&panel, "root/b", cx);
1362
1363    panel.update_in(cx, |panel, window, cx| {
1364        panel.paste(&Default::default(), window, cx);
1365    });
1366    cx.executor().run_until_parked();
1367
1368    assert_eq!(
1369        visible_entries_as_strings(&panel, 0..50, cx),
1370        &[
1371            "v root",
1372            "    v a",
1373            "          one.txt",
1374            "          two.txt",
1375            "    v b",
1376            "          one.txt",
1377            "          two.txt  <== selected",
1378        ],
1379        "Cut entries should only be copied for the second paste!"
1380    );
1381}
1382
1383#[gpui::test]
1384async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1385    init_test(cx);
1386
1387    let fs = FakeFs::new(cx.executor());
1388    fs.insert_tree(
1389        "/root1",
1390        json!({
1391            "one.txt": "",
1392            "two.txt": "",
1393            "three.txt": "",
1394            "a": {
1395                "0": { "q": "", "r": "", "s": "" },
1396                "1": { "t": "", "u": "" },
1397                "2": { "v": "", "w": "", "x": "", "y": "" },
1398            },
1399        }),
1400    )
1401    .await;
1402
1403    fs.insert_tree(
1404        "/root2",
1405        json!({
1406            "one.txt": "",
1407            "two.txt": "",
1408            "four.txt": "",
1409            "b": {
1410                "3": { "Q": "" },
1411                "4": { "R": "", "S": "", "T": "", "U": "" },
1412            },
1413        }),
1414    )
1415    .await;
1416
1417    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1418    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1419    let workspace = window
1420        .read_with(cx, |mw, _| mw.workspace().clone())
1421        .unwrap();
1422    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1423    let panel = workspace.update_in(cx, ProjectPanel::new);
1424    cx.run_until_parked();
1425
1426    select_path(&panel, "root1/three.txt", cx);
1427    panel.update_in(cx, |panel, window, cx| {
1428        panel.cut(&Default::default(), window, cx);
1429    });
1430
1431    select_path(&panel, "root2/one.txt", cx);
1432    panel.update_in(cx, |panel, window, cx| {
1433        panel.select_next(&Default::default(), window, cx);
1434        panel.paste(&Default::default(), window, cx);
1435    });
1436    cx.executor().run_until_parked();
1437    assert_eq!(
1438        visible_entries_as_strings(&panel, 0..50, cx),
1439        &[
1440            //
1441            "v root1",
1442            "    > a",
1443            "      one.txt",
1444            "      two.txt",
1445            "v root2",
1446            "    > b",
1447            "      four.txt",
1448            "      one.txt",
1449            "      three.txt  <== selected  <== marked",
1450            "      two.txt",
1451        ]
1452    );
1453
1454    select_path(&panel, "root1/a", cx);
1455    panel.update_in(cx, |panel, window, cx| {
1456        panel.cut(&Default::default(), window, cx);
1457    });
1458    select_path(&panel, "root2/two.txt", cx);
1459    panel.update_in(cx, |panel, window, cx| {
1460        panel.select_next(&Default::default(), window, cx);
1461        panel.paste(&Default::default(), window, cx);
1462    });
1463
1464    cx.executor().run_until_parked();
1465    assert_eq!(
1466        visible_entries_as_strings(&panel, 0..50, cx),
1467        &[
1468            //
1469            "v root1",
1470            "      one.txt",
1471            "      two.txt",
1472            "v root2",
1473            "    > a  <== selected",
1474            "    > b",
1475            "      four.txt",
1476            "      one.txt",
1477            "      three.txt  <== marked",
1478            "      two.txt",
1479        ]
1480    );
1481}
1482
1483#[gpui::test]
1484async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1485    init_test(cx);
1486
1487    let fs = FakeFs::new(cx.executor());
1488    fs.insert_tree(
1489        "/root1",
1490        json!({
1491            "one.txt": "",
1492            "two.txt": "",
1493            "three.txt": "",
1494            "a": {
1495                "0": { "q": "", "r": "", "s": "" },
1496                "1": { "t": "", "u": "" },
1497                "2": { "v": "", "w": "", "x": "", "y": "" },
1498            },
1499        }),
1500    )
1501    .await;
1502
1503    fs.insert_tree(
1504        "/root2",
1505        json!({
1506            "one.txt": "",
1507            "two.txt": "",
1508            "four.txt": "",
1509            "b": {
1510                "3": { "Q": "" },
1511                "4": { "R": "", "S": "", "T": "", "U": "" },
1512            },
1513        }),
1514    )
1515    .await;
1516
1517    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1518    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1519    let workspace = window
1520        .read_with(cx, |mw, _| mw.workspace().clone())
1521        .unwrap();
1522    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1523    let panel = workspace.update_in(cx, ProjectPanel::new);
1524    cx.run_until_parked();
1525
1526    select_path(&panel, "root1/three.txt", cx);
1527    panel.update_in(cx, |panel, window, cx| {
1528        panel.copy(&Default::default(), window, cx);
1529    });
1530
1531    select_path(&panel, "root2/one.txt", cx);
1532    panel.update_in(cx, |panel, window, cx| {
1533        panel.select_next(&Default::default(), window, cx);
1534        panel.paste(&Default::default(), window, cx);
1535    });
1536    cx.executor().run_until_parked();
1537    assert_eq!(
1538        visible_entries_as_strings(&panel, 0..50, cx),
1539        &[
1540            //
1541            "v root1",
1542            "    > a",
1543            "      one.txt",
1544            "      three.txt",
1545            "      two.txt",
1546            "v root2",
1547            "    > b",
1548            "      four.txt",
1549            "      one.txt",
1550            "      three.txt  <== selected  <== marked",
1551            "      two.txt",
1552        ]
1553    );
1554
1555    select_path(&panel, "root1/three.txt", cx);
1556    panel.update_in(cx, |panel, window, cx| {
1557        panel.copy(&Default::default(), window, cx);
1558    });
1559    select_path(&panel, "root2/two.txt", cx);
1560    panel.update_in(cx, |panel, window, cx| {
1561        panel.select_next(&Default::default(), window, cx);
1562        panel.paste(&Default::default(), window, cx);
1563    });
1564
1565    cx.executor().run_until_parked();
1566    assert_eq!(
1567        visible_entries_as_strings(&panel, 0..50, cx),
1568        &[
1569            //
1570            "v root1",
1571            "    > a",
1572            "      one.txt",
1573            "      three.txt",
1574            "      two.txt",
1575            "v root2",
1576            "    > b",
1577            "      four.txt",
1578            "      one.txt",
1579            "      three.txt",
1580            "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
1581            "      two.txt",
1582        ]
1583    );
1584
1585    panel.update_in(cx, |panel, window, cx| {
1586        panel.cancel(&menu::Cancel {}, window, cx)
1587    });
1588    cx.executor().run_until_parked();
1589
1590    select_path(&panel, "root1/a", cx);
1591    panel.update_in(cx, |panel, window, cx| {
1592        panel.copy(&Default::default(), window, cx);
1593    });
1594    select_path(&panel, "root2/two.txt", cx);
1595    panel.update_in(cx, |panel, window, cx| {
1596        panel.select_next(&Default::default(), window, cx);
1597        panel.paste(&Default::default(), window, cx);
1598    });
1599
1600    cx.executor().run_until_parked();
1601    assert_eq!(
1602        visible_entries_as_strings(&panel, 0..50, cx),
1603        &[
1604            //
1605            "v root1",
1606            "    > a",
1607            "      one.txt",
1608            "      three.txt",
1609            "      two.txt",
1610            "v root2",
1611            "    > a  <== selected",
1612            "    > b",
1613            "      four.txt",
1614            "      one.txt",
1615            "      three.txt",
1616            "      three copy.txt",
1617            "      two.txt",
1618        ]
1619    );
1620}
1621
1622#[gpui::test]
1623async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1624    init_test(cx);
1625
1626    let fs = FakeFs::new(cx.executor());
1627    fs.insert_tree(
1628        "/root",
1629        json!({
1630            "a": {
1631                "one.txt": "",
1632                "two.txt": "",
1633                "inner_dir": {
1634                    "three.txt": "",
1635                    "four.txt": "",
1636                }
1637            },
1638            "b": {}
1639        }),
1640    )
1641    .await;
1642
1643    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1644    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1645    let workspace = window
1646        .read_with(cx, |mw, _| mw.workspace().clone())
1647        .unwrap();
1648    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1649    let panel = workspace.update_in(cx, ProjectPanel::new);
1650    cx.run_until_parked();
1651
1652    select_path(&panel, "root/a", cx);
1653    panel.update_in(cx, |panel, window, cx| {
1654        panel.copy(&Default::default(), window, cx);
1655        panel.select_next(&Default::default(), window, cx);
1656        panel.paste(&Default::default(), window, cx);
1657    });
1658    cx.executor().run_until_parked();
1659
1660    let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1661    assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1662
1663    let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1664    assert_ne!(
1665        pasted_dir_file, None,
1666        "Pasted directory file should have an entry"
1667    );
1668
1669    let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1670    assert_ne!(
1671        pasted_dir_inner_dir, None,
1672        "Directories inside pasted directory should have an entry"
1673    );
1674
1675    toggle_expand_dir(&panel, "root/b/a", cx);
1676    toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1677
1678    assert_eq!(
1679        visible_entries_as_strings(&panel, 0..50, cx),
1680        &[
1681            //
1682            "v root",
1683            "    > a",
1684            "    v b",
1685            "        v a",
1686            "            v inner_dir  <== selected",
1687            "                  four.txt",
1688            "                  three.txt",
1689            "              one.txt",
1690            "              two.txt",
1691        ]
1692    );
1693
1694    select_path(&panel, "root", cx);
1695    panel.update_in(cx, |panel, window, cx| {
1696        panel.paste(&Default::default(), window, cx)
1697    });
1698    cx.executor().run_until_parked();
1699    assert_eq!(
1700        visible_entries_as_strings(&panel, 0..50, cx),
1701        &[
1702            //
1703            "v root",
1704            "    > a",
1705            "    > [EDITOR: 'a copy']  <== selected",
1706            "    v b",
1707            "        v a",
1708            "            v inner_dir",
1709            "                  four.txt",
1710            "                  three.txt",
1711            "              one.txt",
1712            "              two.txt"
1713        ]
1714    );
1715
1716    let confirm = panel.update_in(cx, |panel, window, cx| {
1717        panel
1718            .filename_editor
1719            .update(cx, |editor, cx| editor.set_text("c", window, cx));
1720        panel.confirm_edit(true, window, cx).unwrap()
1721    });
1722    assert_eq!(
1723        visible_entries_as_strings(&panel, 0..50, cx),
1724        &[
1725            //
1726            "v root",
1727            "    > a",
1728            "    > [PROCESSING: 'c']  <== selected",
1729            "    v b",
1730            "        v a",
1731            "            v inner_dir",
1732            "                  four.txt",
1733            "                  three.txt",
1734            "              one.txt",
1735            "              two.txt"
1736        ]
1737    );
1738
1739    confirm.await.unwrap();
1740
1741    panel.update_in(cx, |panel, window, cx| {
1742        panel.paste(&Default::default(), window, cx)
1743    });
1744    cx.executor().run_until_parked();
1745    assert_eq!(
1746        visible_entries_as_strings(&panel, 0..50, cx),
1747        &[
1748            //
1749            "v root",
1750            "    > a",
1751            "    v b",
1752            "        v a",
1753            "            v inner_dir",
1754            "                  four.txt",
1755            "                  three.txt",
1756            "              one.txt",
1757            "              two.txt",
1758            "    v c",
1759            "        > a  <== selected",
1760            "        > inner_dir",
1761            "          one.txt",
1762            "          two.txt",
1763        ]
1764    );
1765}
1766
1767#[gpui::test]
1768async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1769    init_test(cx);
1770
1771    let fs = FakeFs::new(cx.executor());
1772    fs.insert_tree(
1773        "/test",
1774        json!({
1775            "dir1": {
1776                "a.txt": "",
1777                "b.txt": "",
1778            },
1779            "dir2": {},
1780            "c.txt": "",
1781            "d.txt": "",
1782        }),
1783    )
1784    .await;
1785
1786    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1787    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1788    let workspace = window
1789        .read_with(cx, |mw, _| mw.workspace().clone())
1790        .unwrap();
1791    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1792    let panel = workspace.update_in(cx, ProjectPanel::new);
1793    cx.run_until_parked();
1794
1795    toggle_expand_dir(&panel, "test/dir1", cx);
1796
1797    cx.simulate_modifiers_change(gpui::Modifiers {
1798        control: true,
1799        ..Default::default()
1800    });
1801
1802    select_path_with_mark(&panel, "test/dir1", cx);
1803    select_path_with_mark(&panel, "test/c.txt", cx);
1804
1805    assert_eq!(
1806        visible_entries_as_strings(&panel, 0..15, cx),
1807        &[
1808            "v test",
1809            "    v dir1  <== marked",
1810            "          a.txt",
1811            "          b.txt",
1812            "    > dir2",
1813            "      c.txt  <== selected  <== marked",
1814            "      d.txt",
1815        ],
1816        "Initial state before copying dir1 and c.txt"
1817    );
1818
1819    panel.update_in(cx, |panel, window, cx| {
1820        panel.copy(&Default::default(), window, cx);
1821    });
1822    select_path(&panel, "test/dir2", cx);
1823    panel.update_in(cx, |panel, window, cx| {
1824        panel.paste(&Default::default(), window, cx);
1825    });
1826    cx.executor().run_until_parked();
1827
1828    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1829
1830    assert_eq!(
1831        visible_entries_as_strings(&panel, 0..15, cx),
1832        &[
1833            "v test",
1834            "    v dir1  <== marked",
1835            "          a.txt",
1836            "          b.txt",
1837            "    v dir2",
1838            "        v dir1  <== selected",
1839            "              a.txt",
1840            "              b.txt",
1841            "          c.txt",
1842            "      c.txt  <== marked",
1843            "      d.txt",
1844        ],
1845        "Should copy dir1 as well as c.txt into dir2"
1846    );
1847
1848    // Disambiguating multiple files should not open the rename editor.
1849    select_path(&panel, "test/dir2", cx);
1850    panel.update_in(cx, |panel, window, cx| {
1851        panel.paste(&Default::default(), window, cx);
1852    });
1853    cx.executor().run_until_parked();
1854
1855    assert_eq!(
1856        visible_entries_as_strings(&panel, 0..15, cx),
1857        &[
1858            "v test",
1859            "    v dir1  <== marked",
1860            "          a.txt",
1861            "          b.txt",
1862            "    v dir2",
1863            "        v dir1",
1864            "              a.txt",
1865            "              b.txt",
1866            "        > dir1 copy  <== selected",
1867            "          c.txt",
1868            "          c copy.txt",
1869            "      c.txt  <== marked",
1870            "      d.txt",
1871        ],
1872        "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1873    );
1874}
1875
1876#[gpui::test]
1877async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1878    init_test(cx);
1879
1880    let fs = FakeFs::new(cx.executor());
1881    fs.insert_tree(
1882        "/test",
1883        json!({
1884            "dir1": {
1885                "a.txt": "",
1886                "b.txt": "",
1887            },
1888            "dir2": {},
1889            "c.txt": "",
1890            "d.txt": "",
1891        }),
1892    )
1893    .await;
1894
1895    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1896    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1897    let workspace = window
1898        .read_with(cx, |mw, _| mw.workspace().clone())
1899        .unwrap();
1900    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1901    let panel = workspace.update_in(cx, ProjectPanel::new);
1902    cx.run_until_parked();
1903
1904    toggle_expand_dir(&panel, "test/dir1", cx);
1905
1906    cx.simulate_modifiers_change(gpui::Modifiers {
1907        control: true,
1908        ..Default::default()
1909    });
1910
1911    select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1912    select_path_with_mark(&panel, "test/dir1", cx);
1913    select_path_with_mark(&panel, "test/c.txt", cx);
1914
1915    assert_eq!(
1916        visible_entries_as_strings(&panel, 0..15, cx),
1917        &[
1918            "v test",
1919            "    v dir1  <== marked",
1920            "          a.txt  <== marked",
1921            "          b.txt",
1922            "    > dir2",
1923            "      c.txt  <== selected  <== marked",
1924            "      d.txt",
1925        ],
1926        "Initial state before copying a.txt, dir1 and c.txt"
1927    );
1928
1929    panel.update_in(cx, |panel, window, cx| {
1930        panel.copy(&Default::default(), window, cx);
1931    });
1932    select_path(&panel, "test/dir2", cx);
1933    panel.update_in(cx, |panel, window, cx| {
1934        panel.paste(&Default::default(), window, cx);
1935    });
1936    cx.executor().run_until_parked();
1937
1938    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1939
1940    assert_eq!(
1941        visible_entries_as_strings(&panel, 0..20, cx),
1942        &[
1943            "v test",
1944            "    v dir1  <== marked",
1945            "          a.txt  <== marked",
1946            "          b.txt",
1947            "    v dir2",
1948            "        v dir1  <== selected",
1949            "              a.txt",
1950            "              b.txt",
1951            "          c.txt",
1952            "      c.txt  <== marked",
1953            "      d.txt",
1954        ],
1955        "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1956    );
1957}
1958
1959#[gpui::test]
1960async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1961    init_test_with_editor(cx);
1962
1963    let fs = FakeFs::new(cx.executor());
1964    fs.insert_tree(
1965        path!("/src"),
1966        json!({
1967            "test": {
1968                "first.rs": "// First Rust file",
1969                "second.rs": "// Second Rust file",
1970                "third.rs": "// Third Rust file",
1971            }
1972        }),
1973    )
1974    .await;
1975
1976    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1977    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1978    let workspace = window
1979        .read_with(cx, |mw, _| mw.workspace().clone())
1980        .unwrap();
1981    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1982    let panel = workspace.update_in(cx, ProjectPanel::new);
1983    cx.run_until_parked();
1984
1985    toggle_expand_dir(&panel, "src/test", cx);
1986    select_path(&panel, "src/test/first.rs", cx);
1987    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1988    cx.executor().run_until_parked();
1989    assert_eq!(
1990        visible_entries_as_strings(&panel, 0..10, cx),
1991        &[
1992            "v src",
1993            "    v test",
1994            "          first.rs  <== selected  <== marked",
1995            "          second.rs",
1996            "          third.rs"
1997        ]
1998    );
1999    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
2000
2001    submit_deletion(&panel, cx);
2002    assert_eq!(
2003        visible_entries_as_strings(&panel, 0..10, cx),
2004        &[
2005            "v src",
2006            "    v test",
2007            "          second.rs  <== selected",
2008            "          third.rs"
2009        ],
2010        "Project panel should have no deleted file, no other file is selected in it"
2011    );
2012    ensure_no_open_items_and_panes(&workspace, cx);
2013
2014    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2015    cx.executor().run_until_parked();
2016    assert_eq!(
2017        visible_entries_as_strings(&panel, 0..10, cx),
2018        &[
2019            "v src",
2020            "    v test",
2021            "          second.rs  <== selected  <== marked",
2022            "          third.rs"
2023        ]
2024    );
2025    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
2026
2027    workspace.update_in(cx, |workspace, window, cx| {
2028        let active_items = workspace
2029            .panes()
2030            .iter()
2031            .filter_map(|pane| pane.read(cx).active_item())
2032            .collect::<Vec<_>>();
2033        assert_eq!(active_items.len(), 1);
2034        let open_editor = active_items
2035            .into_iter()
2036            .next()
2037            .unwrap()
2038            .downcast::<Editor>()
2039            .expect("Open item should be an editor");
2040        open_editor.update(cx, |editor, cx| {
2041            editor.set_text("Another text!", window, cx)
2042        });
2043    });
2044    submit_deletion_skipping_prompt(&panel, cx);
2045    assert_eq!(
2046        visible_entries_as_strings(&panel, 0..10, cx),
2047        &["v src", "    v test", "          third.rs  <== selected"],
2048        "Project panel should have no deleted file, with one last file remaining"
2049    );
2050    ensure_no_open_items_and_panes(&workspace, cx);
2051}
2052
2053#[gpui::test]
2054async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) {
2055    init_test_with_editor(cx);
2056    set_auto_open_settings(
2057        cx,
2058        ProjectPanelAutoOpenSettings {
2059            on_create: Some(true),
2060            ..Default::default()
2061        },
2062    );
2063
2064    let fs = FakeFs::new(cx.executor());
2065    fs.insert_tree(path!("/root"), json!({})).await;
2066
2067    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2068    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2069    let workspace = window
2070        .read_with(cx, |mw, _| mw.workspace().clone())
2071        .unwrap();
2072    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2073    let panel = workspace.update_in(cx, ProjectPanel::new);
2074    cx.run_until_parked();
2075
2076    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2077    cx.run_until_parked();
2078    panel
2079        .update_in(cx, |panel, window, cx| {
2080            panel.filename_editor.update(cx, |editor, cx| {
2081                editor.set_text("auto-open.rs", window, cx);
2082            });
2083            panel.confirm_edit(true, window, cx).unwrap()
2084        })
2085        .await
2086        .unwrap();
2087    cx.run_until_parked();
2088
2089    ensure_single_file_is_opened(&workspace, "auto-open.rs", cx);
2090}
2091
2092#[gpui::test]
2093async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) {
2094    init_test_with_editor(cx);
2095    set_auto_open_settings(
2096        cx,
2097        ProjectPanelAutoOpenSettings {
2098            on_create: Some(false),
2099            ..Default::default()
2100        },
2101    );
2102
2103    let fs = FakeFs::new(cx.executor());
2104    fs.insert_tree(path!("/root"), json!({})).await;
2105
2106    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2107    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2108    let workspace = window
2109        .read_with(cx, |mw, _| mw.workspace().clone())
2110        .unwrap();
2111    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2112    let panel = workspace.update_in(cx, ProjectPanel::new);
2113    cx.run_until_parked();
2114
2115    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2116    cx.run_until_parked();
2117    panel
2118        .update_in(cx, |panel, window, cx| {
2119            panel.filename_editor.update(cx, |editor, cx| {
2120                editor.set_text("manual-open.rs", window, cx);
2121            });
2122            panel.confirm_edit(true, window, cx).unwrap()
2123        })
2124        .await
2125        .unwrap();
2126    cx.run_until_parked();
2127
2128    ensure_no_open_items_and_panes(&workspace, cx);
2129}
2130
2131#[gpui::test]
2132async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) {
2133    init_test_with_editor(cx);
2134    set_auto_open_settings(
2135        cx,
2136        ProjectPanelAutoOpenSettings {
2137            on_paste: Some(true),
2138            ..Default::default()
2139        },
2140    );
2141
2142    let fs = FakeFs::new(cx.executor());
2143    fs.insert_tree(
2144        path!("/root"),
2145        json!({
2146            "src": {
2147                "original.rs": ""
2148            },
2149            "target": {}
2150        }),
2151    )
2152    .await;
2153
2154    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2155    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2156    let workspace = window
2157        .read_with(cx, |mw, _| mw.workspace().clone())
2158        .unwrap();
2159    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2160    let panel = workspace.update_in(cx, ProjectPanel::new);
2161    cx.run_until_parked();
2162
2163    toggle_expand_dir(&panel, "root/src", cx);
2164    toggle_expand_dir(&panel, "root/target", cx);
2165
2166    select_path(&panel, "root/src/original.rs", cx);
2167    panel.update_in(cx, |panel, window, cx| {
2168        panel.copy(&Default::default(), window, cx);
2169    });
2170
2171    select_path(&panel, "root/target", cx);
2172    panel.update_in(cx, |panel, window, cx| {
2173        panel.paste(&Default::default(), window, cx);
2174    });
2175    cx.executor().run_until_parked();
2176
2177    ensure_single_file_is_opened(&workspace, "target/original.rs", cx);
2178}
2179
2180#[gpui::test]
2181async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) {
2182    init_test_with_editor(cx);
2183    set_auto_open_settings(
2184        cx,
2185        ProjectPanelAutoOpenSettings {
2186            on_paste: Some(false),
2187            ..Default::default()
2188        },
2189    );
2190
2191    let fs = FakeFs::new(cx.executor());
2192    fs.insert_tree(
2193        path!("/root"),
2194        json!({
2195            "src": {
2196                "original.rs": ""
2197            },
2198            "target": {}
2199        }),
2200    )
2201    .await;
2202
2203    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2204    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2205    let workspace = window
2206        .read_with(cx, |mw, _| mw.workspace().clone())
2207        .unwrap();
2208    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2209    let panel = workspace.update_in(cx, ProjectPanel::new);
2210    cx.run_until_parked();
2211
2212    toggle_expand_dir(&panel, "root/src", cx);
2213    toggle_expand_dir(&panel, "root/target", cx);
2214
2215    select_path(&panel, "root/src/original.rs", cx);
2216    panel.update_in(cx, |panel, window, cx| {
2217        panel.copy(&Default::default(), window, cx);
2218    });
2219
2220    select_path(&panel, "root/target", cx);
2221    panel.update_in(cx, |panel, window, cx| {
2222        panel.paste(&Default::default(), window, cx);
2223    });
2224    cx.executor().run_until_parked();
2225
2226    ensure_no_open_items_and_panes(&workspace, cx);
2227    assert!(
2228        find_project_entry(&panel, "root/target/original.rs", cx).is_some(),
2229        "Pasted entry should exist even when auto-open is disabled"
2230    );
2231}
2232
2233#[gpui::test]
2234async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) {
2235    init_test_with_editor(cx);
2236    set_auto_open_settings(
2237        cx,
2238        ProjectPanelAutoOpenSettings {
2239            on_drop: Some(true),
2240            ..Default::default()
2241        },
2242    );
2243
2244    let fs = FakeFs::new(cx.executor());
2245    fs.insert_tree(path!("/root"), json!({})).await;
2246
2247    let temp_dir = tempfile::tempdir().unwrap();
2248    let external_path = temp_dir.path().join("dropped.rs");
2249    std::fs::write(&external_path, "// dropped").unwrap();
2250    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2251        .await;
2252
2253    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2254    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2255    let workspace = window
2256        .read_with(cx, |mw, _| mw.workspace().clone())
2257        .unwrap();
2258    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2259    let panel = workspace.update_in(cx, ProjectPanel::new);
2260    cx.run_until_parked();
2261
2262    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2263    panel.update_in(cx, |panel, window, cx| {
2264        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2265    });
2266    cx.executor().run_until_parked();
2267
2268    ensure_single_file_is_opened(&workspace, "dropped.rs", cx);
2269}
2270
2271#[gpui::test]
2272async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) {
2273    init_test_with_editor(cx);
2274    set_auto_open_settings(
2275        cx,
2276        ProjectPanelAutoOpenSettings {
2277            on_drop: Some(false),
2278            ..Default::default()
2279        },
2280    );
2281
2282    let fs = FakeFs::new(cx.executor());
2283    fs.insert_tree(path!("/root"), json!({})).await;
2284
2285    let temp_dir = tempfile::tempdir().unwrap();
2286    let external_path = temp_dir.path().join("manual.rs");
2287    std::fs::write(&external_path, "// dropped").unwrap();
2288    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2289        .await;
2290
2291    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2292    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2293    let workspace = window
2294        .read_with(cx, |mw, _| mw.workspace().clone())
2295        .unwrap();
2296    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2297    let panel = workspace.update_in(cx, ProjectPanel::new);
2298    cx.run_until_parked();
2299
2300    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2301    panel.update_in(cx, |panel, window, cx| {
2302        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2303    });
2304    cx.executor().run_until_parked();
2305
2306    ensure_no_open_items_and_panes(&workspace, cx);
2307    assert!(
2308        find_project_entry(&panel, "root/manual.rs", cx).is_some(),
2309        "Dropped entry should exist even when auto-open is disabled"
2310    );
2311}
2312
2313#[gpui::test]
2314async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2315    init_test_with_editor(cx);
2316
2317    let fs = FakeFs::new(cx.executor());
2318    fs.insert_tree(
2319        "/src",
2320        json!({
2321            "test": {
2322                "first.rs": "// First Rust file",
2323                "second.rs": "// Second Rust file",
2324                "third.rs": "// Third Rust file",
2325            }
2326        }),
2327    )
2328    .await;
2329
2330    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2331    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2332    let workspace = window
2333        .read_with(cx, |mw, _| mw.workspace().clone())
2334        .unwrap();
2335    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2336    let panel = workspace.update_in(cx, |workspace, window, cx| {
2337        let panel = ProjectPanel::new(workspace, window, cx);
2338        workspace.add_panel(panel.clone(), window, cx);
2339        panel
2340    });
2341    cx.run_until_parked();
2342
2343    select_path(&panel, "src", cx);
2344    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2345    cx.executor().run_until_parked();
2346    assert_eq!(
2347        visible_entries_as_strings(&panel, 0..10, cx),
2348        &[
2349            //
2350            "v src  <== selected",
2351            "    > test"
2352        ]
2353    );
2354    panel.update_in(cx, |panel, window, cx| {
2355        panel.new_directory(&NewDirectory, window, cx)
2356    });
2357    cx.run_until_parked();
2358    panel.update_in(cx, |panel, window, cx| {
2359        assert!(panel.filename_editor.read(cx).is_focused(window));
2360    });
2361    cx.executor().run_until_parked();
2362    assert_eq!(
2363        visible_entries_as_strings(&panel, 0..10, cx),
2364        &[
2365            //
2366            "v src",
2367            "    > [EDITOR: '']  <== selected",
2368            "    > test"
2369        ]
2370    );
2371    panel.update_in(cx, |panel, window, cx| {
2372        panel
2373            .filename_editor
2374            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2375        assert!(
2376            panel.confirm_edit(true, window, cx).is_none(),
2377            "Should not allow to confirm on conflicting new directory name"
2378        );
2379    });
2380    cx.executor().run_until_parked();
2381    panel.update_in(cx, |panel, window, cx| {
2382        assert!(
2383            panel.state.edit_state.is_some(),
2384            "Edit state should not be None after conflicting new directory name"
2385        );
2386        panel.cancel(&menu::Cancel, window, cx);
2387    });
2388    cx.run_until_parked();
2389    assert_eq!(
2390        visible_entries_as_strings(&panel, 0..10, cx),
2391        &[
2392            //
2393            "v src  <== selected",
2394            "    > test"
2395        ],
2396        "File list should be unchanged after failed folder create confirmation"
2397    );
2398
2399    select_path(&panel, "src/test", cx);
2400    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2401    cx.executor().run_until_parked();
2402    assert_eq!(
2403        visible_entries_as_strings(&panel, 0..10, cx),
2404        &[
2405            //
2406            "v src",
2407            "    > test  <== selected"
2408        ]
2409    );
2410    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2411    cx.run_until_parked();
2412    panel.update_in(cx, |panel, window, cx| {
2413        assert!(panel.filename_editor.read(cx).is_focused(window));
2414    });
2415    assert_eq!(
2416        visible_entries_as_strings(&panel, 0..10, cx),
2417        &[
2418            "v src",
2419            "    v test",
2420            "          [EDITOR: '']  <== selected",
2421            "          first.rs",
2422            "          second.rs",
2423            "          third.rs"
2424        ]
2425    );
2426    panel.update_in(cx, |panel, window, cx| {
2427        panel
2428            .filename_editor
2429            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2430        assert!(
2431            panel.confirm_edit(true, window, cx).is_none(),
2432            "Should not allow to confirm on conflicting new file name"
2433        );
2434    });
2435    cx.executor().run_until_parked();
2436    panel.update_in(cx, |panel, window, cx| {
2437        assert!(
2438            panel.state.edit_state.is_some(),
2439            "Edit state should not be None after conflicting new file name"
2440        );
2441        panel.cancel(&menu::Cancel, window, cx);
2442    });
2443    cx.run_until_parked();
2444    assert_eq!(
2445        visible_entries_as_strings(&panel, 0..10, cx),
2446        &[
2447            "v src",
2448            "    v test  <== selected",
2449            "          first.rs",
2450            "          second.rs",
2451            "          third.rs"
2452        ],
2453        "File list should be unchanged after failed file create confirmation"
2454    );
2455
2456    select_path(&panel, "src/test/first.rs", cx);
2457    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2458    cx.executor().run_until_parked();
2459    assert_eq!(
2460        visible_entries_as_strings(&panel, 0..10, cx),
2461        &[
2462            "v src",
2463            "    v test",
2464            "          first.rs  <== selected",
2465            "          second.rs",
2466            "          third.rs"
2467        ],
2468    );
2469    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2470    cx.executor().run_until_parked();
2471    panel.update_in(cx, |panel, window, cx| {
2472        assert!(panel.filename_editor.read(cx).is_focused(window));
2473    });
2474    assert_eq!(
2475        visible_entries_as_strings(&panel, 0..10, cx),
2476        &[
2477            "v src",
2478            "    v test",
2479            "          [EDITOR: 'first.rs']  <== selected",
2480            "          second.rs",
2481            "          third.rs"
2482        ]
2483    );
2484    panel.update_in(cx, |panel, window, cx| {
2485        panel
2486            .filename_editor
2487            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2488        assert!(
2489            panel.confirm_edit(true, window, cx).is_none(),
2490            "Should not allow to confirm on conflicting file rename"
2491        )
2492    });
2493    cx.executor().run_until_parked();
2494    panel.update_in(cx, |panel, window, cx| {
2495        assert!(
2496            panel.state.edit_state.is_some(),
2497            "Edit state should not be None after conflicting file rename"
2498        );
2499        panel.cancel(&menu::Cancel, window, cx);
2500    });
2501    cx.executor().run_until_parked();
2502    assert_eq!(
2503        visible_entries_as_strings(&panel, 0..10, cx),
2504        &[
2505            "v src",
2506            "    v test",
2507            "          first.rs  <== selected",
2508            "          second.rs",
2509            "          third.rs"
2510        ],
2511        "File list should be unchanged after failed rename confirmation"
2512    );
2513}
2514
2515// NOTE: This test is skipped on Windows, because on Windows,
2516// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
2517// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
2518#[gpui::test]
2519#[cfg_attr(target_os = "windows", ignore)]
2520async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
2521    init_test_with_editor(cx);
2522
2523    let fs = FakeFs::new(cx.executor());
2524    fs.insert_tree(
2525        "/src",
2526        json!({
2527            "test": {
2528                "first.txt": "// First Txt file",
2529                "second.txt": "// Second Txt file",
2530                "third.txt": "// Third Txt file",
2531            }
2532        }),
2533    )
2534    .await;
2535
2536    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2537    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2538    let workspace = window
2539        .read_with(cx, |mw, _| mw.workspace().clone())
2540        .unwrap();
2541    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2542    let panel = workspace.update_in(cx, |workspace, window, cx| {
2543        let panel = ProjectPanel::new(workspace, window, cx);
2544        workspace.add_panel(panel.clone(), window, cx);
2545        panel
2546    });
2547    cx.run_until_parked();
2548
2549    select_path(&panel, "src", cx);
2550    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2551    cx.executor().run_until_parked();
2552    assert_eq!(
2553        visible_entries_as_strings(&panel, 0..10, cx),
2554        &[
2555            //
2556            "v src  <== selected",
2557            "    > test"
2558        ]
2559    );
2560    panel.update_in(cx, |panel, window, cx| {
2561        panel.new_directory(&NewDirectory, window, cx)
2562    });
2563    cx.run_until_parked();
2564    panel.update_in(cx, |panel, window, cx| {
2565        assert!(panel.filename_editor.read(cx).is_focused(window));
2566    });
2567    cx.executor().run_until_parked();
2568    assert_eq!(
2569        visible_entries_as_strings(&panel, 0..10, cx),
2570        &[
2571            //
2572            "v src",
2573            "    > [EDITOR: '']  <== selected",
2574            "    > test"
2575        ]
2576    );
2577    panel.update_in(cx, |panel, window, cx| {
2578        panel
2579            .filename_editor
2580            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2581        assert!(
2582            panel.confirm_edit(true, window, cx).is_none(),
2583            "Should not allow to confirm on conflicting new directory name"
2584        );
2585    });
2586    cx.executor().run_until_parked();
2587    panel.update_in(cx, |panel, window, cx| {
2588        assert!(
2589            panel.state.edit_state.is_some(),
2590            "Edit state should not be None after conflicting new directory name"
2591        );
2592        panel.cancel(&menu::Cancel, window, cx);
2593    });
2594    cx.run_until_parked();
2595    assert_eq!(
2596        visible_entries_as_strings(&panel, 0..10, cx),
2597        &[
2598            //
2599            "v src  <== selected",
2600            "    > test"
2601        ],
2602        "File list should be unchanged after failed folder create confirmation"
2603    );
2604
2605    select_path(&panel, "src/test", cx);
2606    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2607    cx.executor().run_until_parked();
2608    assert_eq!(
2609        visible_entries_as_strings(&panel, 0..10, cx),
2610        &[
2611            //
2612            "v src",
2613            "    > test  <== selected"
2614        ]
2615    );
2616    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2617    cx.run_until_parked();
2618    panel.update_in(cx, |panel, window, cx| {
2619        assert!(panel.filename_editor.read(cx).is_focused(window));
2620    });
2621    assert_eq!(
2622        visible_entries_as_strings(&panel, 0..10, cx),
2623        &[
2624            "v src",
2625            "    v test",
2626            "          [EDITOR: '']  <== selected",
2627            "          first.txt",
2628            "          second.txt",
2629            "          third.txt"
2630        ]
2631    );
2632    panel.update_in(cx, |panel, window, cx| {
2633        panel
2634            .filename_editor
2635            .update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
2636        assert!(
2637            panel.confirm_edit(true, window, cx).is_none(),
2638            "Should not allow to confirm on conflicting new file name"
2639        );
2640    });
2641    cx.executor().run_until_parked();
2642    panel.update_in(cx, |panel, window, cx| {
2643        assert!(
2644            panel.state.edit_state.is_some(),
2645            "Edit state should not be None after conflicting new file name"
2646        );
2647        panel.cancel(&menu::Cancel, window, cx);
2648    });
2649    cx.run_until_parked();
2650    assert_eq!(
2651        visible_entries_as_strings(&panel, 0..10, cx),
2652        &[
2653            "v src",
2654            "    v test  <== selected",
2655            "          first.txt",
2656            "          second.txt",
2657            "          third.txt"
2658        ],
2659        "File list should be unchanged after failed file create confirmation"
2660    );
2661
2662    select_path(&panel, "src/test/first.txt", cx);
2663    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2664    cx.executor().run_until_parked();
2665    assert_eq!(
2666        visible_entries_as_strings(&panel, 0..10, cx),
2667        &[
2668            "v src",
2669            "    v test",
2670            "          first.txt  <== selected",
2671            "          second.txt",
2672            "          third.txt"
2673        ],
2674    );
2675    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2676    cx.executor().run_until_parked();
2677    panel.update_in(cx, |panel, window, cx| {
2678        assert!(panel.filename_editor.read(cx).is_focused(window));
2679    });
2680    assert_eq!(
2681        visible_entries_as_strings(&panel, 0..10, cx),
2682        &[
2683            "v src",
2684            "    v test",
2685            "          [EDITOR: 'first.txt']  <== selected",
2686            "          second.txt",
2687            "          third.txt"
2688        ]
2689    );
2690    panel.update_in(cx, |panel, window, cx| {
2691        panel
2692            .filename_editor
2693            .update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
2694        assert!(
2695            panel.confirm_edit(true, window, cx).is_none(),
2696            "Should not allow to confirm on conflicting file rename"
2697        )
2698    });
2699    cx.executor().run_until_parked();
2700    panel.update_in(cx, |panel, window, cx| {
2701        assert!(
2702            panel.state.edit_state.is_some(),
2703            "Edit state should not be None after conflicting file rename"
2704        );
2705        panel.cancel(&menu::Cancel, window, cx);
2706    });
2707    cx.executor().run_until_parked();
2708    assert_eq!(
2709        visible_entries_as_strings(&panel, 0..10, cx),
2710        &[
2711            "v src",
2712            "    v test",
2713            "          first.txt  <== selected",
2714            "          second.txt",
2715            "          third.txt"
2716        ],
2717        "File list should be unchanged after failed rename confirmation"
2718    );
2719    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2720    cx.executor().run_until_parked();
2721    // Try to duplicate and check history
2722    panel.update_in(cx, |panel, window, cx| {
2723        panel.duplicate(&Duplicate, window, cx)
2724    });
2725    cx.executor().run_until_parked();
2726
2727    assert_eq!(
2728        visible_entries_as_strings(&panel, 0..10, cx),
2729        &[
2730            "v src",
2731            "    v test",
2732            "          first.txt",
2733            "          [EDITOR: 'first copy.txt']  <== selected  <== marked",
2734            "          second.txt",
2735            "          third.txt"
2736        ],
2737    );
2738
2739    let confirm = panel.update_in(cx, |panel, window, cx| {
2740        panel
2741            .filename_editor
2742            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2743        panel.confirm_edit(true, window, cx).unwrap()
2744    });
2745    confirm.await.unwrap();
2746    cx.executor().run_until_parked();
2747
2748    assert_eq!(
2749        visible_entries_as_strings(&panel, 0..10, cx),
2750        &[
2751            "v src",
2752            "    v test",
2753            "          first.txt",
2754            "          fourth.txt  <== selected",
2755            "          second.txt",
2756            "          third.txt"
2757        ],
2758        "File list should be different after rename confirmation"
2759    );
2760
2761    panel.update_in(cx, |panel, window, cx| {
2762        panel.update_visible_entries(None, false, false, window, cx);
2763    });
2764    cx.executor().run_until_parked();
2765
2766    select_path(&panel, "src/test/first.txt", cx);
2767    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2768    cx.executor().run_until_parked();
2769
2770    workspace.read_with(cx, |this, cx| {
2771        assert!(
2772            this.recent_navigation_history_iter(cx)
2773                .any(|(project_path, abs_path)| {
2774                    project_path.path == Arc::from(rel_path("test/fourth.txt"))
2775                        && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2776                })
2777        );
2778    });
2779}
2780
2781// NOTE: This test is skipped on Windows, because on Windows,
2782// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
2783// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
2784#[gpui::test]
2785#[cfg_attr(target_os = "windows", ignore)]
2786async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
2787    init_test_with_editor(cx);
2788
2789    let fs = FakeFs::new(cx.executor());
2790    fs.insert_tree(
2791        "/src",
2792        json!({
2793            "test": {
2794                "first.txt": "// First Txt file",
2795                "second.txt": "// Second Txt file",
2796                "third.txt": "// Third Txt file",
2797            }
2798        }),
2799    )
2800    .await;
2801
2802    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2803    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2804    let workspace = window
2805        .read_with(cx, |mw, _| mw.workspace().clone())
2806        .unwrap();
2807    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2808    let panel = workspace.update_in(cx, |workspace, window, cx| {
2809        let panel = ProjectPanel::new(workspace, window, cx);
2810        workspace.add_panel(panel.clone(), window, cx);
2811        panel
2812    });
2813    cx.run_until_parked();
2814
2815    select_path(&panel, "src", cx);
2816    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2817    cx.executor().run_until_parked();
2818    assert_eq!(
2819        visible_entries_as_strings(&panel, 0..10, cx),
2820        &[
2821            //
2822            "v src  <== selected",
2823            "    > test"
2824        ]
2825    );
2826
2827    select_path(&panel, "src/test", cx);
2828    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2829    cx.executor().run_until_parked();
2830    assert_eq!(
2831        visible_entries_as_strings(&panel, 0..10, cx),
2832        &[
2833            //
2834            "v src",
2835            "    > test  <== selected"
2836        ]
2837    );
2838    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2839    cx.run_until_parked();
2840    panel.update_in(cx, |panel, window, cx| {
2841        assert!(panel.filename_editor.read(cx).is_focused(window));
2842    });
2843
2844    select_path(&panel, "src/test/first.txt", cx);
2845    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2846    cx.executor().run_until_parked();
2847
2848    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2849    cx.executor().run_until_parked();
2850
2851    assert_eq!(
2852        visible_entries_as_strings(&panel, 0..10, cx),
2853        &[
2854            "v src",
2855            "    v test",
2856            "          [EDITOR: 'first.txt']  <== selected  <== marked",
2857            "          second.txt",
2858            "          third.txt"
2859        ],
2860    );
2861
2862    let confirm = panel.update_in(cx, |panel, window, cx| {
2863        panel
2864            .filename_editor
2865            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2866        panel.confirm_edit(true, window, cx).unwrap()
2867    });
2868    confirm.await.unwrap();
2869    cx.executor().run_until_parked();
2870
2871    assert_eq!(
2872        visible_entries_as_strings(&panel, 0..10, cx),
2873        &[
2874            "v src",
2875            "    v test",
2876            "          fourth.txt  <== selected",
2877            "          second.txt",
2878            "          third.txt"
2879        ],
2880        "File list should be different after rename confirmation"
2881    );
2882
2883    panel.update_in(cx, |panel, window, cx| {
2884        panel.update_visible_entries(None, false, false, window, cx);
2885    });
2886    cx.executor().run_until_parked();
2887
2888    select_path(&panel, "src/test/second.txt", cx);
2889    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2890    cx.executor().run_until_parked();
2891
2892    workspace.read_with(cx, |this, cx| {
2893        assert!(
2894            this.recent_navigation_history_iter(cx)
2895                .any(|(project_path, abs_path)| {
2896                    project_path.path == Arc::from(rel_path("test/fourth.txt"))
2897                        && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2898                })
2899        );
2900    });
2901}
2902
2903#[gpui::test]
2904async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2905    init_test_with_editor(cx);
2906
2907    let fs = FakeFs::new(cx.executor());
2908    fs.insert_tree(
2909        path!("/root"),
2910        json!({
2911            "tree1": {
2912                ".git": {},
2913                "dir1": {
2914                    "modified1.txt": "1",
2915                    "unmodified1.txt": "1",
2916                    "modified2.txt": "1",
2917                },
2918                "dir2": {
2919                    "modified3.txt": "1",
2920                    "unmodified2.txt": "1",
2921                },
2922                "modified4.txt": "1",
2923                "unmodified3.txt": "1",
2924            },
2925            "tree2": {
2926                ".git": {},
2927                "dir3": {
2928                    "modified5.txt": "1",
2929                    "unmodified4.txt": "1",
2930                },
2931                "modified6.txt": "1",
2932                "unmodified5.txt": "1",
2933            }
2934        }),
2935    )
2936    .await;
2937
2938    // Mark files as git modified
2939    fs.set_head_and_index_for_repo(
2940        path!("/root/tree1/.git").as_ref(),
2941        &[
2942            ("dir1/modified1.txt", "modified".into()),
2943            ("dir1/modified2.txt", "modified".into()),
2944            ("modified4.txt", "modified".into()),
2945            ("dir2/modified3.txt", "modified".into()),
2946        ],
2947    );
2948    fs.set_head_and_index_for_repo(
2949        path!("/root/tree2/.git").as_ref(),
2950        &[
2951            ("dir3/modified5.txt", "modified".into()),
2952            ("modified6.txt", "modified".into()),
2953        ],
2954    );
2955
2956    let project = Project::test(
2957        fs.clone(),
2958        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2959        cx,
2960    )
2961    .await;
2962
2963    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2964        let mut worktrees = project.worktrees(cx);
2965        let worktree1 = worktrees.next().unwrap();
2966        let worktree2 = worktrees.next().unwrap();
2967        (
2968            worktree1.read(cx).as_local().unwrap().scan_complete(),
2969            worktree2.read(cx).as_local().unwrap().scan_complete(),
2970        )
2971    });
2972    scan1_complete.await;
2973    scan2_complete.await;
2974    cx.run_until_parked();
2975
2976    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2977    let workspace = window
2978        .read_with(cx, |mw, _| mw.workspace().clone())
2979        .unwrap();
2980    let cx = &mut VisualTestContext::from_window(window.into(), cx);
2981    let panel = workspace.update_in(cx, ProjectPanel::new);
2982    cx.run_until_parked();
2983
2984    // Check initial state
2985    assert_eq!(
2986        visible_entries_as_strings(&panel, 0..15, cx),
2987        &[
2988            "v tree1",
2989            "    > .git",
2990            "    > dir1",
2991            "    > dir2",
2992            "      modified4.txt",
2993            "      unmodified3.txt",
2994            "v tree2",
2995            "    > .git",
2996            "    > dir3",
2997            "      modified6.txt",
2998            "      unmodified5.txt"
2999        ],
3000    );
3001
3002    // Test selecting next modified entry
3003    panel.update_in(cx, |panel, window, cx| {
3004        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3005    });
3006    cx.run_until_parked();
3007
3008    assert_eq!(
3009        visible_entries_as_strings(&panel, 0..6, cx),
3010        &[
3011            "v tree1",
3012            "    > .git",
3013            "    v dir1",
3014            "          modified1.txt  <== selected",
3015            "          modified2.txt",
3016            "          unmodified1.txt",
3017        ],
3018    );
3019
3020    panel.update_in(cx, |panel, window, cx| {
3021        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3022    });
3023    cx.run_until_parked();
3024
3025    assert_eq!(
3026        visible_entries_as_strings(&panel, 0..6, cx),
3027        &[
3028            "v tree1",
3029            "    > .git",
3030            "    v dir1",
3031            "          modified1.txt",
3032            "          modified2.txt  <== selected",
3033            "          unmodified1.txt",
3034        ],
3035    );
3036
3037    panel.update_in(cx, |panel, window, cx| {
3038        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3039    });
3040    cx.run_until_parked();
3041
3042    assert_eq!(
3043        visible_entries_as_strings(&panel, 6..9, cx),
3044        &[
3045            "    v dir2",
3046            "          modified3.txt  <== selected",
3047            "          unmodified2.txt",
3048        ],
3049    );
3050
3051    panel.update_in(cx, |panel, window, cx| {
3052        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3053    });
3054    cx.run_until_parked();
3055
3056    assert_eq!(
3057        visible_entries_as_strings(&panel, 9..11, cx),
3058        &["      modified4.txt  <== selected", "      unmodified3.txt",],
3059    );
3060
3061    panel.update_in(cx, |panel, window, cx| {
3062        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3063    });
3064    cx.run_until_parked();
3065
3066    assert_eq!(
3067        visible_entries_as_strings(&panel, 13..16, cx),
3068        &[
3069            "    v dir3",
3070            "          modified5.txt  <== selected",
3071            "          unmodified4.txt",
3072        ],
3073    );
3074
3075    panel.update_in(cx, |panel, window, cx| {
3076        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3077    });
3078    cx.run_until_parked();
3079
3080    assert_eq!(
3081        visible_entries_as_strings(&panel, 16..18, cx),
3082        &["      modified6.txt  <== selected", "      unmodified5.txt",],
3083    );
3084
3085    // Wraps around to first modified file
3086    panel.update_in(cx, |panel, window, cx| {
3087        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3088    });
3089    cx.run_until_parked();
3090
3091    assert_eq!(
3092        visible_entries_as_strings(&panel, 0..18, cx),
3093        &[
3094            "v tree1",
3095            "    > .git",
3096            "    v dir1",
3097            "          modified1.txt  <== selected",
3098            "          modified2.txt",
3099            "          unmodified1.txt",
3100            "    v dir2",
3101            "          modified3.txt",
3102            "          unmodified2.txt",
3103            "      modified4.txt",
3104            "      unmodified3.txt",
3105            "v tree2",
3106            "    > .git",
3107            "    v dir3",
3108            "          modified5.txt",
3109            "          unmodified4.txt",
3110            "      modified6.txt",
3111            "      unmodified5.txt",
3112        ],
3113    );
3114
3115    // Wraps around again to last modified file
3116    panel.update_in(cx, |panel, window, cx| {
3117        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3118    });
3119    cx.run_until_parked();
3120
3121    assert_eq!(
3122        visible_entries_as_strings(&panel, 16..18, cx),
3123        &["      modified6.txt  <== selected", "      unmodified5.txt",],
3124    );
3125
3126    panel.update_in(cx, |panel, window, cx| {
3127        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3128    });
3129    cx.run_until_parked();
3130
3131    assert_eq!(
3132        visible_entries_as_strings(&panel, 13..16, cx),
3133        &[
3134            "    v dir3",
3135            "          modified5.txt  <== selected",
3136            "          unmodified4.txt",
3137        ],
3138    );
3139
3140    panel.update_in(cx, |panel, window, cx| {
3141        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3142    });
3143    cx.run_until_parked();
3144
3145    assert_eq!(
3146        visible_entries_as_strings(&panel, 9..11, cx),
3147        &["      modified4.txt  <== selected", "      unmodified3.txt",],
3148    );
3149
3150    panel.update_in(cx, |panel, window, cx| {
3151        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3152    });
3153    cx.run_until_parked();
3154
3155    assert_eq!(
3156        visible_entries_as_strings(&panel, 6..9, cx),
3157        &[
3158            "    v dir2",
3159            "          modified3.txt  <== selected",
3160            "          unmodified2.txt",
3161        ],
3162    );
3163
3164    panel.update_in(cx, |panel, window, cx| {
3165        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3166    });
3167    cx.run_until_parked();
3168
3169    assert_eq!(
3170        visible_entries_as_strings(&panel, 0..6, cx),
3171        &[
3172            "v tree1",
3173            "    > .git",
3174            "    v dir1",
3175            "          modified1.txt",
3176            "          modified2.txt  <== selected",
3177            "          unmodified1.txt",
3178        ],
3179    );
3180
3181    panel.update_in(cx, |panel, window, cx| {
3182        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3183    });
3184    cx.run_until_parked();
3185
3186    assert_eq!(
3187        visible_entries_as_strings(&panel, 0..6, cx),
3188        &[
3189            "v tree1",
3190            "    > .git",
3191            "    v dir1",
3192            "          modified1.txt  <== selected",
3193            "          modified2.txt",
3194            "          unmodified1.txt",
3195        ],
3196    );
3197}
3198
3199#[gpui::test]
3200async fn test_select_directory(cx: &mut gpui::TestAppContext) {
3201    init_test_with_editor(cx);
3202
3203    let fs = FakeFs::new(cx.executor());
3204    fs.insert_tree(
3205        "/project_root",
3206        json!({
3207            "dir_1": {
3208                "nested_dir": {
3209                    "file_a.py": "# File contents",
3210                }
3211            },
3212            "file_1.py": "# File contents",
3213            "dir_2": {
3214
3215            },
3216            "dir_3": {
3217
3218            },
3219            "file_2.py": "# File contents",
3220            "dir_4": {
3221
3222            },
3223        }),
3224    )
3225    .await;
3226
3227    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3228    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3229    let workspace = window
3230        .read_with(cx, |mw, _| mw.workspace().clone())
3231        .unwrap();
3232    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3233    let panel = workspace.update_in(cx, ProjectPanel::new);
3234    cx.run_until_parked();
3235
3236    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3237    cx.executor().run_until_parked();
3238    select_path(&panel, "project_root/dir_1", cx);
3239    cx.executor().run_until_parked();
3240    assert_eq!(
3241        visible_entries_as_strings(&panel, 0..10, cx),
3242        &[
3243            "v project_root",
3244            "    > dir_1  <== selected",
3245            "    > dir_2",
3246            "    > dir_3",
3247            "    > dir_4",
3248            "      file_1.py",
3249            "      file_2.py",
3250        ]
3251    );
3252    panel.update_in(cx, |panel, window, cx| {
3253        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3254    });
3255
3256    assert_eq!(
3257        visible_entries_as_strings(&panel, 0..10, cx),
3258        &[
3259            "v project_root  <== selected",
3260            "    > dir_1",
3261            "    > dir_2",
3262            "    > dir_3",
3263            "    > dir_4",
3264            "      file_1.py",
3265            "      file_2.py",
3266        ]
3267    );
3268
3269    panel.update_in(cx, |panel, window, cx| {
3270        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3271    });
3272
3273    assert_eq!(
3274        visible_entries_as_strings(&panel, 0..10, cx),
3275        &[
3276            "v project_root",
3277            "    > dir_1",
3278            "    > dir_2",
3279            "    > dir_3",
3280            "    > dir_4  <== selected",
3281            "      file_1.py",
3282            "      file_2.py",
3283        ]
3284    );
3285
3286    panel.update_in(cx, |panel, window, cx| {
3287        panel.select_next_directory(&SelectNextDirectory, window, cx)
3288    });
3289
3290    assert_eq!(
3291        visible_entries_as_strings(&panel, 0..10, cx),
3292        &[
3293            "v project_root  <== selected",
3294            "    > dir_1",
3295            "    > dir_2",
3296            "    > dir_3",
3297            "    > dir_4",
3298            "      file_1.py",
3299            "      file_2.py",
3300        ]
3301    );
3302}
3303
3304#[gpui::test]
3305async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
3306    init_test_with_editor(cx);
3307
3308    let fs = FakeFs::new(cx.executor());
3309    fs.insert_tree(
3310        "/project_root",
3311        json!({
3312            "dir_1": {
3313                "nested_dir": {
3314                    "file_a.py": "# File contents",
3315                }
3316            },
3317            "file_1.py": "# File contents",
3318            "file_2.py": "# File contents",
3319            "zdir_2": {
3320                "nested_dir2": {
3321                    "file_b.py": "# File contents",
3322                }
3323            },
3324        }),
3325    )
3326    .await;
3327
3328    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3329    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3330    let workspace = window
3331        .read_with(cx, |mw, _| mw.workspace().clone())
3332        .unwrap();
3333    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3334    let panel = workspace.update_in(cx, ProjectPanel::new);
3335    cx.run_until_parked();
3336
3337    assert_eq!(
3338        visible_entries_as_strings(&panel, 0..10, cx),
3339        &[
3340            "v project_root",
3341            "    > dir_1",
3342            "    > zdir_2",
3343            "      file_1.py",
3344            "      file_2.py",
3345        ]
3346    );
3347    panel.update_in(cx, |panel, window, cx| {
3348        panel.select_first(&SelectFirst, window, cx)
3349    });
3350
3351    assert_eq!(
3352        visible_entries_as_strings(&panel, 0..10, cx),
3353        &[
3354            "v project_root  <== selected",
3355            "    > dir_1",
3356            "    > zdir_2",
3357            "      file_1.py",
3358            "      file_2.py",
3359        ]
3360    );
3361
3362    panel.update_in(cx, |panel, window, cx| {
3363        panel.select_last(&SelectLast, window, cx)
3364    });
3365
3366    assert_eq!(
3367        visible_entries_as_strings(&panel, 0..10, cx),
3368        &[
3369            "v project_root",
3370            "    > dir_1",
3371            "    > zdir_2",
3372            "      file_1.py",
3373            "      file_2.py  <== selected",
3374        ]
3375    );
3376
3377    cx.update(|_, cx| {
3378        let settings = *ProjectPanelSettings::get_global(cx);
3379        ProjectPanelSettings::override_global(
3380            ProjectPanelSettings {
3381                hide_root: true,
3382                ..settings
3383            },
3384            cx,
3385        );
3386    });
3387
3388    let panel = workspace.update_in(cx, ProjectPanel::new);
3389    cx.run_until_parked();
3390
3391    #[rustfmt::skip]
3392    assert_eq!(
3393        visible_entries_as_strings(&panel, 0..10, cx),
3394        &[
3395            "> dir_1",
3396            "> zdir_2",
3397            "  file_1.py",
3398            "  file_2.py",
3399        ],
3400        "With hide_root=true, root should be hidden"
3401    );
3402
3403    panel.update_in(cx, |panel, window, cx| {
3404        panel.select_first(&SelectFirst, window, cx)
3405    });
3406
3407    assert_eq!(
3408        visible_entries_as_strings(&panel, 0..10, cx),
3409        &[
3410            "> dir_1  <== selected",
3411            "> zdir_2",
3412            "  file_1.py",
3413            "  file_2.py",
3414        ],
3415        "With hide_root=true, first entry should be dir_1, not the hidden root"
3416    );
3417}
3418
3419#[gpui::test]
3420async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
3421    init_test_with_editor(cx);
3422
3423    let fs = FakeFs::new(cx.executor());
3424    fs.insert_tree(
3425        "/project_root",
3426        json!({
3427            "dir_1": {
3428                "nested_dir": {
3429                    "file_a.py": "# File contents",
3430                }
3431            },
3432            "file_1.py": "# File contents",
3433        }),
3434    )
3435    .await;
3436
3437    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3438    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3439    let workspace = window
3440        .read_with(cx, |mw, _| mw.workspace().clone())
3441        .unwrap();
3442    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3443    let panel = workspace.update_in(cx, ProjectPanel::new);
3444    cx.run_until_parked();
3445
3446    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3447    cx.executor().run_until_parked();
3448    select_path(&panel, "project_root/dir_1", cx);
3449    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3450    select_path(&panel, "project_root/dir_1/nested_dir", cx);
3451    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3452    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3453    cx.executor().run_until_parked();
3454    assert_eq!(
3455        visible_entries_as_strings(&panel, 0..10, cx),
3456        &[
3457            "v project_root",
3458            "    v dir_1",
3459            "        > nested_dir  <== selected",
3460            "      file_1.py",
3461        ]
3462    );
3463}
3464
3465#[gpui::test]
3466async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
3467    init_test_with_editor(cx);
3468
3469    let fs = FakeFs::new(cx.executor());
3470    fs.insert_tree(
3471        "/project_root",
3472        json!({
3473            "dir_1": {
3474                "nested_dir": {
3475                    "file_a.py": "# File contents",
3476                    "file_b.py": "# File contents",
3477                    "file_c.py": "# File contents",
3478                },
3479                "file_1.py": "# File contents",
3480                "file_2.py": "# File contents",
3481                "file_3.py": "# File contents",
3482            },
3483            "dir_2": {
3484                "file_1.py": "# File contents",
3485                "file_2.py": "# File contents",
3486                "file_3.py": "# File contents",
3487            }
3488        }),
3489    )
3490    .await;
3491
3492    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3493    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3494    let workspace = window
3495        .read_with(cx, |mw, _| mw.workspace().clone())
3496        .unwrap();
3497    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3498    let panel = workspace.update_in(cx, ProjectPanel::new);
3499    cx.run_until_parked();
3500
3501    panel.update_in(cx, |panel, window, cx| {
3502        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3503    });
3504    cx.executor().run_until_parked();
3505    assert_eq!(
3506        visible_entries_as_strings(&panel, 0..10, cx),
3507        &["v project_root", "    > dir_1", "    > dir_2",]
3508    );
3509
3510    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
3511    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3512    cx.executor().run_until_parked();
3513    assert_eq!(
3514        visible_entries_as_strings(&panel, 0..10, cx),
3515        &[
3516            "v project_root",
3517            "    v dir_1  <== selected",
3518            "        > nested_dir",
3519            "          file_1.py",
3520            "          file_2.py",
3521            "          file_3.py",
3522            "    > dir_2",
3523        ]
3524    );
3525}
3526
3527#[gpui::test]
3528async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
3529    init_test_with_editor(cx);
3530
3531    let fs = FakeFs::new(cx.executor());
3532    let worktree_content = json!({
3533        "dir_1": {
3534            "file_1.py": "# File contents",
3535        },
3536        "dir_2": {
3537            "file_1.py": "# File contents",
3538        }
3539    });
3540
3541    fs.insert_tree("/project_root_1", worktree_content.clone())
3542        .await;
3543    fs.insert_tree("/project_root_2", worktree_content).await;
3544
3545    let project = Project::test(
3546        fs.clone(),
3547        ["/project_root_1".as_ref(), "/project_root_2".as_ref()],
3548        cx,
3549    )
3550    .await;
3551    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3552    let workspace = window
3553        .read_with(cx, |mw, _| mw.workspace().clone())
3554        .unwrap();
3555    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3556    let panel = workspace.update_in(cx, ProjectPanel::new);
3557    cx.run_until_parked();
3558
3559    panel.update_in(cx, |panel, window, cx| {
3560        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3561    });
3562    cx.executor().run_until_parked();
3563    assert_eq!(
3564        visible_entries_as_strings(&panel, 0..10, cx),
3565        &["> project_root_1", "> project_root_2",]
3566    );
3567}
3568
3569#[gpui::test]
3570async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) {
3571    init_test_with_editor(cx);
3572
3573    let fs = FakeFs::new(cx.executor());
3574    fs.insert_tree(
3575        "/project_root",
3576        json!({
3577            "dir_1": {
3578                "nested_dir": {
3579                    "file_a.py": "# File contents",
3580                    "file_b.py": "# File contents",
3581                    "file_c.py": "# File contents",
3582                },
3583                "file_1.py": "# File contents",
3584                "file_2.py": "# File contents",
3585                "file_3.py": "# File contents",
3586            },
3587            "dir_2": {
3588                "file_1.py": "# File contents",
3589                "file_2.py": "# File contents",
3590                "file_3.py": "# File contents",
3591            }
3592        }),
3593    )
3594    .await;
3595
3596    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3597    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3598    let workspace = window
3599        .read_with(cx, |mw, _| mw.workspace().clone())
3600        .unwrap();
3601    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3602    let panel = workspace.update_in(cx, ProjectPanel::new);
3603    cx.run_until_parked();
3604
3605    // Open project_root/dir_1 to ensure that a nested directory is expanded
3606    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3607    cx.executor().run_until_parked();
3608    assert_eq!(
3609        visible_entries_as_strings(&panel, 0..10, cx),
3610        &[
3611            "v project_root",
3612            "    v dir_1  <== selected",
3613            "        > nested_dir",
3614            "          file_1.py",
3615            "          file_2.py",
3616            "          file_3.py",
3617            "    > dir_2",
3618        ]
3619    );
3620
3621    // Close root directory
3622    toggle_expand_dir(&panel, "project_root", cx);
3623    cx.executor().run_until_parked();
3624    assert_eq!(
3625        visible_entries_as_strings(&panel, 0..10, cx),
3626        &["> project_root  <== selected"]
3627    );
3628
3629    // Run collapse_all_entries and make sure root is not expanded
3630    panel.update_in(cx, |panel, window, cx| {
3631        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3632    });
3633    cx.executor().run_until_parked();
3634    assert_eq!(
3635        visible_entries_as_strings(&panel, 0..10, cx),
3636        &["> project_root  <== selected"]
3637    );
3638}
3639
3640#[gpui::test]
3641async fn test_collapse_all_entries_with_invisible_worktree(cx: &mut gpui::TestAppContext) {
3642    init_test_with_editor(cx);
3643
3644    let fs = FakeFs::new(cx.executor());
3645    fs.insert_tree(
3646        "/project_root",
3647        json!({
3648            "dir_1": {
3649                "nested_dir": {
3650                    "file_a.py": "# File contents",
3651                },
3652                "file_1.py": "# File contents",
3653            },
3654            "dir_2": {
3655                "file_1.py": "# File contents",
3656            }
3657        }),
3658    )
3659    .await;
3660    fs.insert_tree(
3661        "/external",
3662        json!({
3663            "external_file.py": "# External file",
3664        }),
3665    )
3666    .await;
3667
3668    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3669    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3670    let workspace = window
3671        .read_with(cx, |mw, _| mw.workspace().clone())
3672        .unwrap();
3673    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3674    let panel = workspace.update_in(cx, ProjectPanel::new);
3675    cx.run_until_parked();
3676
3677    let (_invisible_worktree, _) = project
3678        .update(cx, |project, cx| {
3679            project.find_or_create_worktree("/external/external_file.py", false, cx)
3680        })
3681        .await
3682        .unwrap();
3683    cx.run_until_parked();
3684
3685    assert_eq!(
3686        visible_entries_as_strings(&panel, 0..10, cx),
3687        &["v project_root", "    > dir_1", "    > dir_2",],
3688        "invisible worktree should not appear in project panel"
3689    );
3690
3691    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3692    cx.executor().run_until_parked();
3693
3694    panel.update_in(cx, |panel, window, cx| {
3695        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3696    });
3697    cx.executor().run_until_parked();
3698    assert_eq!(
3699        visible_entries_as_strings(&panel, 0..10, cx),
3700        &["v project_root", "    > dir_1  <== selected", "    > dir_2",],
3701        "with single visible worktree, root should stay expanded even if invisible worktrees exist"
3702    );
3703}
3704
3705#[gpui::test]
3706async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
3707    init_test(cx);
3708
3709    let fs = FakeFs::new(cx.executor());
3710    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
3711    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
3712    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3713    let workspace = window
3714        .read_with(cx, |mw, _| mw.workspace().clone())
3715        .unwrap();
3716    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3717    let panel = workspace.update_in(cx, ProjectPanel::new);
3718    cx.run_until_parked();
3719
3720    // Make a new buffer with no backing file
3721    workspace.update_in(cx, |workspace, window, cx| {
3722        Editor::new_file(workspace, &Default::default(), window, cx)
3723    });
3724
3725    cx.executor().run_until_parked();
3726
3727    // "Save as" the buffer, creating a new backing file for it
3728    let save_task = workspace.update_in(cx, |workspace, window, cx| {
3729        workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3730    });
3731
3732    cx.executor().run_until_parked();
3733    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
3734    save_task.await.unwrap();
3735
3736    // Rename the file
3737    select_path(&panel, "root/new", cx);
3738    assert_eq!(
3739        visible_entries_as_strings(&panel, 0..10, cx),
3740        &["v root", "      new  <== selected  <== marked"]
3741    );
3742    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3743    panel.update_in(cx, |panel, window, cx| {
3744        panel
3745            .filename_editor
3746            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
3747    });
3748    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3749
3750    cx.executor().run_until_parked();
3751    assert_eq!(
3752        visible_entries_as_strings(&panel, 0..10, cx),
3753        &["v root", "      newer  <== selected"]
3754    );
3755
3756    workspace
3757        .update_in(cx, |workspace, window, cx| {
3758            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3759        })
3760        .await
3761        .unwrap();
3762
3763    cx.executor().run_until_parked();
3764    // assert that saving the file doesn't restore "new"
3765    assert_eq!(
3766        visible_entries_as_strings(&panel, 0..10, cx),
3767        &["v root", "      newer  <== selected"]
3768    );
3769}
3770
3771// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
3772// you can't rename a directory which some program has already open. This is a
3773// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
3774// to it, and thus renaming it will fail on Windows.
3775// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
3776// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
3777#[gpui::test]
3778#[cfg_attr(target_os = "windows", ignore)]
3779async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
3780    init_test_with_editor(cx);
3781
3782    let fs = FakeFs::new(cx.executor());
3783    fs.insert_tree(
3784        "/root1",
3785        json!({
3786            "dir1": {
3787                "file1.txt": "content 1",
3788            },
3789        }),
3790    )
3791    .await;
3792
3793    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3794    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3795    let workspace = window
3796        .read_with(cx, |mw, _| mw.workspace().clone())
3797        .unwrap();
3798    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3799    let panel = workspace.update_in(cx, ProjectPanel::new);
3800    cx.run_until_parked();
3801
3802    toggle_expand_dir(&panel, "root1/dir1", cx);
3803
3804    assert_eq!(
3805        visible_entries_as_strings(&panel, 0..20, cx),
3806        &["v root1", "    v dir1  <== selected", "          file1.txt",],
3807        "Initial state with worktrees"
3808    );
3809
3810    select_path(&panel, "root1", cx);
3811    assert_eq!(
3812        visible_entries_as_strings(&panel, 0..20, cx),
3813        &["v root1  <== selected", "    v dir1", "          file1.txt",],
3814    );
3815
3816    // Rename root1 to new_root1
3817    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3818
3819    assert_eq!(
3820        visible_entries_as_strings(&panel, 0..20, cx),
3821        &[
3822            "v [EDITOR: 'root1']  <== selected",
3823            "    v dir1",
3824            "          file1.txt",
3825        ],
3826    );
3827
3828    let confirm = panel.update_in(cx, |panel, window, cx| {
3829        panel
3830            .filename_editor
3831            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
3832        panel.confirm_edit(true, window, cx).unwrap()
3833    });
3834    confirm.await.unwrap();
3835    cx.run_until_parked();
3836    assert_eq!(
3837        visible_entries_as_strings(&panel, 0..20, cx),
3838        &[
3839            "v new_root1  <== selected",
3840            "    v dir1",
3841            "          file1.txt",
3842        ],
3843        "Should update worktree name"
3844    );
3845
3846    // Ensure internal paths have been updated
3847    select_path(&panel, "new_root1/dir1/file1.txt", cx);
3848    assert_eq!(
3849        visible_entries_as_strings(&panel, 0..20, cx),
3850        &[
3851            "v new_root1",
3852            "    v dir1",
3853            "          file1.txt  <== selected",
3854        ],
3855        "Files in renamed worktree are selectable"
3856    );
3857}
3858
3859#[gpui::test]
3860async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
3861    init_test_with_editor(cx);
3862
3863    let fs = FakeFs::new(cx.executor());
3864    fs.insert_tree(
3865        "/root1",
3866        json!({
3867            "dir1": { "file1.txt": "content" },
3868            "file2.txt": "content",
3869        }),
3870    )
3871    .await;
3872    fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
3873        .await;
3874
3875    // Test 1: Single worktree, hide_root=true - rename should be blocked
3876    {
3877        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3878        let window =
3879            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3880        let workspace = window
3881            .read_with(cx, |mw, _| mw.workspace().clone())
3882            .unwrap();
3883        let cx = &mut VisualTestContext::from_window(window.into(), cx);
3884
3885        cx.update(|_, cx| {
3886            let settings = *ProjectPanelSettings::get_global(cx);
3887            ProjectPanelSettings::override_global(
3888                ProjectPanelSettings {
3889                    hide_root: true,
3890                    ..settings
3891                },
3892                cx,
3893            );
3894        });
3895
3896        let panel = workspace.update_in(cx, ProjectPanel::new);
3897        cx.run_until_parked();
3898
3899        panel.update(cx, |panel, cx| {
3900            let project = panel.project.read(cx);
3901            let worktree = project.visible_worktrees(cx).next().unwrap();
3902            let root_entry = worktree.read(cx).root_entry().unwrap();
3903            panel.selection = Some(SelectedEntry {
3904                worktree_id: worktree.read(cx).id(),
3905                entry_id: root_entry.id,
3906            });
3907        });
3908
3909        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3910
3911        assert!(
3912            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3913            "Rename should be blocked when hide_root=true with single worktree"
3914        );
3915    }
3916
3917    // Test 2: Multiple worktrees, hide_root=true - rename should work
3918    {
3919        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3920        let window =
3921            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3922        let workspace = window
3923            .read_with(cx, |mw, _| mw.workspace().clone())
3924            .unwrap();
3925        let cx = &mut VisualTestContext::from_window(window.into(), cx);
3926
3927        cx.update(|_, cx| {
3928            let settings = *ProjectPanelSettings::get_global(cx);
3929            ProjectPanelSettings::override_global(
3930                ProjectPanelSettings {
3931                    hide_root: true,
3932                    ..settings
3933                },
3934                cx,
3935            );
3936        });
3937
3938        let panel = workspace.update_in(cx, ProjectPanel::new);
3939        cx.run_until_parked();
3940
3941        select_path(&panel, "root1", cx);
3942        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3943
3944        #[cfg(target_os = "windows")]
3945        assert!(
3946            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3947            "Rename should be blocked on Windows even with multiple worktrees"
3948        );
3949
3950        #[cfg(not(target_os = "windows"))]
3951        {
3952            assert!(
3953                panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()),
3954                "Rename should work with multiple worktrees on non-Windows when hide_root=true"
3955            );
3956            panel.update_in(cx, |panel, window, cx| {
3957                panel.cancel(&menu::Cancel, window, cx)
3958            });
3959        }
3960    }
3961}
3962
3963#[gpui::test]
3964async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
3965    init_test_with_editor(cx);
3966    let fs = FakeFs::new(cx.executor());
3967    fs.insert_tree(
3968        "/project_root",
3969        json!({
3970            "dir_1": {
3971                "nested_dir": {
3972                    "file_a.py": "# File contents",
3973                }
3974            },
3975            "file_1.py": "# File contents",
3976        }),
3977    )
3978    .await;
3979
3980    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3981    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
3982    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3983    let workspace = window
3984        .read_with(cx, |mw, _| mw.workspace().clone())
3985        .unwrap();
3986    let cx = &mut VisualTestContext::from_window(window.into(), cx);
3987    let panel = workspace.update_in(cx, ProjectPanel::new);
3988    cx.run_until_parked();
3989
3990    cx.update(|window, cx| {
3991        panel.update(cx, |this, cx| {
3992            this.select_next(&Default::default(), window, cx);
3993            this.expand_selected_entry(&Default::default(), window, cx);
3994        })
3995    });
3996    cx.run_until_parked();
3997
3998    cx.update(|window, cx| {
3999        panel.update(cx, |this, cx| {
4000            this.expand_selected_entry(&Default::default(), window, cx);
4001        })
4002    });
4003    cx.run_until_parked();
4004
4005    cx.update(|window, cx| {
4006        panel.update(cx, |this, cx| {
4007            this.select_next(&Default::default(), window, cx);
4008            this.expand_selected_entry(&Default::default(), window, cx);
4009        })
4010    });
4011    cx.run_until_parked();
4012
4013    cx.update(|window, cx| {
4014        panel.update(cx, |this, cx| {
4015            this.select_next(&Default::default(), window, cx);
4016        })
4017    });
4018    cx.run_until_parked();
4019
4020    assert_eq!(
4021        visible_entries_as_strings(&panel, 0..10, cx),
4022        &[
4023            "v project_root",
4024            "    v dir_1",
4025            "        v nested_dir",
4026            "              file_a.py  <== selected",
4027            "      file_1.py",
4028        ]
4029    );
4030    let modifiers_with_shift = gpui::Modifiers {
4031        shift: true,
4032        ..Default::default()
4033    };
4034    cx.run_until_parked();
4035    cx.simulate_modifiers_change(modifiers_with_shift);
4036    cx.update(|window, cx| {
4037        panel.update(cx, |this, cx| {
4038            this.select_next(&Default::default(), window, cx);
4039        })
4040    });
4041    assert_eq!(
4042        visible_entries_as_strings(&panel, 0..10, cx),
4043        &[
4044            "v project_root",
4045            "    v dir_1",
4046            "        v nested_dir",
4047            "              file_a.py",
4048            "      file_1.py  <== selected  <== marked",
4049        ]
4050    );
4051    cx.update(|window, cx| {
4052        panel.update(cx, |this, cx| {
4053            this.select_previous(&Default::default(), window, cx);
4054        })
4055    });
4056    assert_eq!(
4057        visible_entries_as_strings(&panel, 0..10, cx),
4058        &[
4059            "v project_root",
4060            "    v dir_1",
4061            "        v nested_dir",
4062            "              file_a.py  <== selected  <== marked",
4063            "      file_1.py  <== marked",
4064        ]
4065    );
4066    cx.update(|window, cx| {
4067        panel.update(cx, |this, cx| {
4068            let drag = DraggedSelection {
4069                active_selection: this.selection.unwrap(),
4070                marked_selections: this.marked_entries.clone().into(),
4071            };
4072            let target_entry = this
4073                .project
4074                .read(cx)
4075                .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
4076                .unwrap();
4077            this.drag_onto(&drag, target_entry.id, false, window, cx);
4078        });
4079    });
4080    cx.run_until_parked();
4081    assert_eq!(
4082        visible_entries_as_strings(&panel, 0..10, cx),
4083        &[
4084            "v project_root",
4085            "    v dir_1",
4086            "        v nested_dir",
4087            "      file_1.py  <== marked",
4088            "      file_a.py  <== selected  <== marked",
4089        ]
4090    );
4091    // ESC clears out all marks
4092    cx.update(|window, cx| {
4093        panel.update(cx, |this, cx| {
4094            this.cancel(&menu::Cancel, window, cx);
4095        })
4096    });
4097    cx.executor().run_until_parked();
4098    assert_eq!(
4099        visible_entries_as_strings(&panel, 0..10, cx),
4100        &[
4101            "v project_root",
4102            "    v dir_1",
4103            "        v nested_dir",
4104            "      file_1.py",
4105            "      file_a.py  <== selected",
4106        ]
4107    );
4108    // ESC clears out all marks
4109    cx.update(|window, cx| {
4110        panel.update(cx, |this, cx| {
4111            this.select_previous(&SelectPrevious, window, cx);
4112            this.select_next(&SelectNext, window, cx);
4113        })
4114    });
4115    assert_eq!(
4116        visible_entries_as_strings(&panel, 0..10, cx),
4117        &[
4118            "v project_root",
4119            "    v dir_1",
4120            "        v nested_dir",
4121            "      file_1.py  <== marked",
4122            "      file_a.py  <== selected  <== marked",
4123        ]
4124    );
4125    cx.simulate_modifiers_change(Default::default());
4126    cx.update(|window, cx| {
4127        panel.update(cx, |this, cx| {
4128            this.cut(&Cut, window, cx);
4129            this.select_previous(&SelectPrevious, window, cx);
4130            this.select_previous(&SelectPrevious, window, cx);
4131
4132            this.paste(&Paste, window, cx);
4133            this.update_visible_entries(None, false, false, window, cx);
4134        })
4135    });
4136    cx.run_until_parked();
4137    assert_eq!(
4138        visible_entries_as_strings(&panel, 0..10, cx),
4139        &[
4140            "v project_root",
4141            "    v dir_1",
4142            "        v nested_dir",
4143            "              file_1.py  <== marked",
4144            "              file_a.py  <== selected  <== marked",
4145        ]
4146    );
4147    cx.simulate_modifiers_change(modifiers_with_shift);
4148    cx.update(|window, cx| {
4149        panel.update(cx, |this, cx| {
4150            this.expand_selected_entry(&Default::default(), window, cx);
4151            this.select_next(&SelectNext, window, cx);
4152            this.select_next(&SelectNext, window, cx);
4153        })
4154    });
4155    submit_deletion(&panel, cx);
4156    assert_eq!(
4157        visible_entries_as_strings(&panel, 0..10, cx),
4158        &[
4159            "v project_root",
4160            "    v dir_1",
4161            "        v nested_dir  <== selected",
4162        ]
4163    );
4164}
4165
4166#[gpui::test]
4167async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
4168    init_test(cx);
4169
4170    let fs = FakeFs::new(cx.executor());
4171    fs.insert_tree(
4172        "/root",
4173        json!({
4174            "a": {
4175                "b": {
4176                    "c": {
4177                        "d": {}
4178                    }
4179                }
4180            },
4181            "target_destination": {}
4182        }),
4183    )
4184    .await;
4185
4186    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4187    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4188    let workspace = window
4189        .read_with(cx, |mw, _| mw.workspace().clone())
4190        .unwrap();
4191    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4192
4193    cx.update(|_, cx| {
4194        let settings = *ProjectPanelSettings::get_global(cx);
4195        ProjectPanelSettings::override_global(
4196            ProjectPanelSettings {
4197                auto_fold_dirs: true,
4198                ..settings
4199            },
4200            cx,
4201        );
4202    });
4203
4204    let panel = workspace.update_in(cx, ProjectPanel::new);
4205    cx.run_until_parked();
4206
4207    // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
4208    select_path(&panel, "root/a/b/c/d", cx);
4209    panel.update_in(cx, |panel, window, cx| {
4210        let drag = DraggedSelection {
4211            active_selection: *panel.selection.as_ref().unwrap(),
4212            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
4213        };
4214        let target_entry = panel
4215            .project
4216            .read(cx)
4217            .visible_worktrees(cx)
4218            .next()
4219            .unwrap()
4220            .read(cx)
4221            .entry_for_path(rel_path("target_destination"))
4222            .unwrap();
4223        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4224    });
4225    cx.executor().run_until_parked();
4226
4227    assert_eq!(
4228        visible_entries_as_strings(&panel, 0..10, cx),
4229        &[
4230            "v root",
4231            "    > a/b/c",
4232            "    > target_destination/d  <== selected"
4233        ],
4234        "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
4235    );
4236
4237    // Reset
4238    select_path(&panel, "root/target_destination/d", cx);
4239    panel.update_in(cx, |panel, window, cx| {
4240        let drag = DraggedSelection {
4241            active_selection: *panel.selection.as_ref().unwrap(),
4242            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
4243        };
4244        let target_entry = panel
4245            .project
4246            .read(cx)
4247            .visible_worktrees(cx)
4248            .next()
4249            .unwrap()
4250            .read(cx)
4251            .entry_for_path(rel_path("a/b/c"))
4252            .unwrap();
4253        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4254    });
4255    cx.executor().run_until_parked();
4256
4257    // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
4258    select_path(&panel, "root/a/b", cx);
4259    panel.update_in(cx, |panel, window, cx| {
4260        let drag = DraggedSelection {
4261            active_selection: *panel.selection.as_ref().unwrap(),
4262            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
4263        };
4264        let target_entry = panel
4265            .project
4266            .read(cx)
4267            .visible_worktrees(cx)
4268            .next()
4269            .unwrap()
4270            .read(cx)
4271            .entry_for_path(rel_path("target_destination"))
4272            .unwrap();
4273        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4274    });
4275    cx.executor().run_until_parked();
4276
4277    assert_eq!(
4278        visible_entries_as_strings(&panel, 0..10, cx),
4279        &["v root", "    v a", "    > target_destination/b/c/d"],
4280        "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
4281    );
4282
4283    // Reset
4284    select_path(&panel, "root/target_destination/b", cx);
4285    panel.update_in(cx, |panel, window, cx| {
4286        let drag = DraggedSelection {
4287            active_selection: *panel.selection.as_ref().unwrap(),
4288            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
4289        };
4290        let target_entry = panel
4291            .project
4292            .read(cx)
4293            .visible_worktrees(cx)
4294            .next()
4295            .unwrap()
4296            .read(cx)
4297            .entry_for_path(rel_path("a"))
4298            .unwrap();
4299        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4300    });
4301    cx.executor().run_until_parked();
4302
4303    // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
4304    select_path(&panel, "root/a", cx);
4305    panel.update_in(cx, |panel, window, cx| {
4306        let drag = DraggedSelection {
4307            active_selection: *panel.selection.as_ref().unwrap(),
4308            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
4309        };
4310        let target_entry = panel
4311            .project
4312            .read(cx)
4313            .visible_worktrees(cx)
4314            .next()
4315            .unwrap()
4316            .read(cx)
4317            .entry_for_path(rel_path("target_destination"))
4318            .unwrap();
4319        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4320    });
4321    cx.executor().run_until_parked();
4322
4323    assert_eq!(
4324        visible_entries_as_strings(&panel, 0..10, cx),
4325        &["v root", "    > target_destination/a/b/c/d"],
4326        "Moving first directory 'a' should move whole 'a/b/c/d' chain"
4327    );
4328}
4329
4330#[gpui::test]
4331async fn test_drag_marked_entries_in_folded_directories(cx: &mut gpui::TestAppContext) {
4332    init_test(cx);
4333
4334    let fs = FakeFs::new(cx.executor());
4335    fs.insert_tree(
4336        "/root",
4337        json!({
4338            "a": {
4339                "b": {
4340                    "c": {}
4341                }
4342            },
4343            "e": {
4344                "f": {
4345                    "g": {}
4346                }
4347            },
4348            "target": {}
4349        }),
4350    )
4351    .await;
4352
4353    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4354    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4355    let workspace = window
4356        .read_with(cx, |mw, _| mw.workspace().clone())
4357        .unwrap();
4358    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4359
4360    cx.update(|_, cx| {
4361        let settings = *ProjectPanelSettings::get_global(cx);
4362        ProjectPanelSettings::override_global(
4363            ProjectPanelSettings {
4364                auto_fold_dirs: true,
4365                ..settings
4366            },
4367            cx,
4368        );
4369    });
4370
4371    let panel = workspace.update_in(cx, ProjectPanel::new);
4372    cx.run_until_parked();
4373
4374    assert_eq!(
4375        visible_entries_as_strings(&panel, 0..10, cx),
4376        &["v root", "    > a/b/c", "    > e/f/g", "    > target"]
4377    );
4378
4379    select_folded_path_with_mark(&panel, "root/a/b/c", "root/a/b", cx);
4380    select_folded_path_with_mark(&panel, "root/e/f/g", "root/e/f", cx);
4381
4382    panel.update_in(cx, |panel, window, cx| {
4383        let drag = DraggedSelection {
4384            active_selection: *panel.selection.as_ref().unwrap(),
4385            marked_selections: panel.marked_entries.clone().into(),
4386        };
4387        let target_entry = panel
4388            .project
4389            .read(cx)
4390            .visible_worktrees(cx)
4391            .next()
4392            .unwrap()
4393            .read(cx)
4394            .entry_for_path(rel_path("target"))
4395            .unwrap();
4396        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4397    });
4398    cx.executor().run_until_parked();
4399
4400    // After dragging 'b/c' and 'f/g' should be moved to target
4401    assert_eq!(
4402        visible_entries_as_strings(&panel, 0..10, cx),
4403        &[
4404            "v root",
4405            "    > a",
4406            "    > e",
4407            "    v target",
4408            "        > b/c",
4409            "        > f/g  <== selected  <== marked"
4410        ],
4411        "Should move 'b/c' and 'f/g' to target, leaving 'a' and 'e'"
4412    );
4413}
4414
4415#[gpui::test]
4416async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4417    init_test(cx);
4418
4419    let fs = FakeFs::new(cx.executor());
4420    fs.insert_tree(
4421        "/root_a",
4422        json!({
4423            "src": {
4424                "lib.rs": "",
4425                "main.rs": ""
4426            },
4427            "docs": {
4428                "guide.md": ""
4429            },
4430            "multi": {
4431                "alpha.txt": "",
4432                "beta.txt": ""
4433            }
4434        }),
4435    )
4436    .await;
4437    fs.insert_tree(
4438        "/root_b",
4439        json!({
4440            "dst": {
4441                "existing.md": ""
4442            },
4443            "target.txt": ""
4444        }),
4445    )
4446    .await;
4447
4448    let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
4449    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4450    let workspace = window
4451        .read_with(cx, |mw, _| mw.workspace().clone())
4452        .unwrap();
4453    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4454    let panel = workspace.update_in(cx, ProjectPanel::new);
4455    cx.run_until_parked();
4456
4457    // Case 1: move a file onto a directory in another worktree.
4458    select_path(&panel, "root_a/src/main.rs", cx);
4459    drag_selection_to(&panel, "root_b/dst", false, cx);
4460    assert!(
4461        find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(),
4462        "Dragged file should appear under destination worktree"
4463    );
4464    assert_eq!(
4465        find_project_entry(&panel, "root_a/src/main.rs", cx),
4466        None,
4467        "Dragged file should be removed from the source worktree"
4468    );
4469
4470    // Case 2: drop a file onto another worktree file so it lands in the parent directory.
4471    select_path(&panel, "root_a/docs/guide.md", cx);
4472    drag_selection_to(&panel, "root_b/dst/existing.md", true, cx);
4473    assert!(
4474        find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(),
4475        "Dropping onto a file should place the entry beside the target file"
4476    );
4477    assert_eq!(
4478        find_project_entry(&panel, "root_a/docs/guide.md", cx),
4479        None,
4480        "Source file should be removed after the move"
4481    );
4482
4483    // Case 3: move an entire directory.
4484    select_path(&panel, "root_a/src", cx);
4485    drag_selection_to(&panel, "root_b/dst", false, cx);
4486    assert!(
4487        find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(),
4488        "Dragging a directory should move its nested contents"
4489    );
4490    assert_eq!(
4491        find_project_entry(&panel, "root_a/src", cx),
4492        None,
4493        "Directory should no longer exist in the source worktree"
4494    );
4495
4496    // Case 4: multi-selection drag between worktrees.
4497    panel.update(cx, |panel, _| panel.marked_entries.clear());
4498    select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx);
4499    select_path_with_mark(&panel, "root_a/multi/beta.txt", cx);
4500    drag_selection_to(&panel, "root_b/dst", false, cx);
4501    assert!(
4502        find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some()
4503            && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(),
4504        "All marked entries should move to the destination worktree"
4505    );
4506    assert_eq!(
4507        find_project_entry(&panel, "root_a/multi/alpha.txt", cx),
4508        None,
4509        "Marked entries should be removed from the origin worktree"
4510    );
4511    assert_eq!(
4512        find_project_entry(&panel, "root_a/multi/beta.txt", cx),
4513        None,
4514        "Marked entries should be removed from the origin worktree"
4515    );
4516}
4517
4518#[gpui::test]
4519async fn test_drag_multiple_entries(cx: &mut gpui::TestAppContext) {
4520    init_test(cx);
4521
4522    let fs = FakeFs::new(cx.executor());
4523    fs.insert_tree(
4524        "/root",
4525        json!({
4526            "src": {
4527                "folder1": {
4528                    "mod.rs": "// folder1 mod"
4529                },
4530                "folder2": {
4531                    "mod.rs": "// folder2 mod"
4532                },
4533                "folder3": {
4534                    "mod.rs": "// folder3 mod",
4535                    "helper.rs": "// helper"
4536                },
4537                "main.rs": ""
4538            }
4539        }),
4540    )
4541    .await;
4542
4543    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4544    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4545    let workspace = window
4546        .read_with(cx, |mw, _| mw.workspace().clone())
4547        .unwrap();
4548    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4549    let panel = workspace.update_in(cx, ProjectPanel::new);
4550    cx.run_until_parked();
4551
4552    toggle_expand_dir(&panel, "root/src", cx);
4553    toggle_expand_dir(&panel, "root/src/folder1", cx);
4554    toggle_expand_dir(&panel, "root/src/folder2", cx);
4555    toggle_expand_dir(&panel, "root/src/folder3", cx);
4556    cx.run_until_parked();
4557
4558    // Case 1: Dragging a folder and a file from a sibling folder together.
4559    panel.update(cx, |panel, _| panel.marked_entries.clear());
4560    select_path_with_mark(&panel, "root/src/folder1", cx);
4561    select_path_with_mark(&panel, "root/src/folder2/mod.rs", cx);
4562
4563    drag_selection_to(&panel, "root", false, cx);
4564
4565    assert!(
4566        find_project_entry(&panel, "root/folder1", cx).is_some(),
4567        "folder1 should be at root after drag"
4568    );
4569    assert!(
4570        find_project_entry(&panel, "root/folder1/mod.rs", cx).is_some(),
4571        "folder1/mod.rs should still be inside folder1 after drag"
4572    );
4573    assert_eq!(
4574        find_project_entry(&panel, "root/src/folder1", cx),
4575        None,
4576        "folder1 should no longer be in src"
4577    );
4578    assert!(
4579        find_project_entry(&panel, "root/mod.rs", cx).is_some(),
4580        "mod.rs from folder2 should be at root"
4581    );
4582
4583    // Case 2: Dragging a folder and its own child together.
4584    panel.update(cx, |panel, _| panel.marked_entries.clear());
4585    select_path_with_mark(&panel, "root/src/folder3", cx);
4586    select_path_with_mark(&panel, "root/src/folder3/mod.rs", cx);
4587
4588    drag_selection_to(&panel, "root", false, cx);
4589
4590    assert!(
4591        find_project_entry(&panel, "root/folder3", cx).is_some(),
4592        "folder3 should be at root after drag"
4593    );
4594    assert!(
4595        find_project_entry(&panel, "root/folder3/mod.rs", cx).is_some(),
4596        "folder3/mod.rs should still be inside folder3"
4597    );
4598    assert!(
4599        find_project_entry(&panel, "root/folder3/helper.rs", cx).is_some(),
4600        "folder3/helper.rs should still be inside folder3"
4601    );
4602}
4603
4604#[gpui::test]
4605async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
4606    init_test_with_editor(cx);
4607    cx.update(|cx| {
4608        cx.update_global::<SettingsStore, _>(|store, cx| {
4609            store.update_user_settings(cx, |settings| {
4610                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4611                settings
4612                    .project_panel
4613                    .get_or_insert_default()
4614                    .auto_reveal_entries = Some(false);
4615            });
4616        })
4617    });
4618
4619    let fs = FakeFs::new(cx.background_executor.clone());
4620    fs.insert_tree(
4621        "/project_root",
4622        json!({
4623            ".git": {},
4624            ".gitignore": "**/gitignored_dir",
4625            "dir_1": {
4626                "file_1.py": "# File 1_1 contents",
4627                "file_2.py": "# File 1_2 contents",
4628                "file_3.py": "# File 1_3 contents",
4629                "gitignored_dir": {
4630                    "file_a.py": "# File contents",
4631                    "file_b.py": "# File contents",
4632                    "file_c.py": "# File contents",
4633                },
4634            },
4635            "dir_2": {
4636                "file_1.py": "# File 2_1 contents",
4637                "file_2.py": "# File 2_2 contents",
4638                "file_3.py": "# File 2_3 contents",
4639            }
4640        }),
4641    )
4642    .await;
4643
4644    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4645    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4646    let workspace = window
4647        .read_with(cx, |mw, _| mw.workspace().clone())
4648        .unwrap();
4649    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4650    let panel = workspace.update_in(cx, ProjectPanel::new);
4651    cx.run_until_parked();
4652
4653    assert_eq!(
4654        visible_entries_as_strings(&panel, 0..20, cx),
4655        &[
4656            "v project_root",
4657            "    > .git",
4658            "    > dir_1",
4659            "    > dir_2",
4660            "      .gitignore",
4661        ]
4662    );
4663
4664    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4665        .expect("dir 1 file is not ignored and should have an entry");
4666    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4667        .expect("dir 2 file is not ignored and should have an entry");
4668    let gitignored_dir_file =
4669        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4670    assert_eq!(
4671        gitignored_dir_file, None,
4672        "File in the gitignored dir should not have an entry before its dir is toggled"
4673    );
4674
4675    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4676    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4677    cx.executor().run_until_parked();
4678    assert_eq!(
4679        visible_entries_as_strings(&panel, 0..20, cx),
4680        &[
4681            "v project_root",
4682            "    > .git",
4683            "    v dir_1",
4684            "        v gitignored_dir  <== selected",
4685            "              file_a.py",
4686            "              file_b.py",
4687            "              file_c.py",
4688            "          file_1.py",
4689            "          file_2.py",
4690            "          file_3.py",
4691            "    > dir_2",
4692            "      .gitignore",
4693        ],
4694        "Should show gitignored dir file list in the project panel"
4695    );
4696    let gitignored_dir_file =
4697        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4698            .expect("after gitignored dir got opened, a file entry should be present");
4699
4700    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4701    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4702    assert_eq!(
4703        visible_entries_as_strings(&panel, 0..20, cx),
4704        &[
4705            "v project_root",
4706            "    > .git",
4707            "    > dir_1  <== selected",
4708            "    > dir_2",
4709            "      .gitignore",
4710        ],
4711        "Should hide all dir contents again and prepare for the auto reveal test"
4712    );
4713
4714    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4715        panel.update(cx, |panel, cx| {
4716            panel.project.update(cx, |_, cx| {
4717                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4718            })
4719        });
4720        cx.run_until_parked();
4721        assert_eq!(
4722            visible_entries_as_strings(&panel, 0..20, cx),
4723            &[
4724                "v project_root",
4725                "    > .git",
4726                "    > dir_1  <== selected",
4727                "    > dir_2",
4728                "      .gitignore",
4729            ],
4730            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4731        );
4732    }
4733
4734    cx.update(|_, cx| {
4735        cx.update_global::<SettingsStore, _>(|store, cx| {
4736            store.update_user_settings(cx, |settings| {
4737                settings
4738                    .project_panel
4739                    .get_or_insert_default()
4740                    .auto_reveal_entries = Some(true)
4741            });
4742        })
4743    });
4744
4745    panel.update(cx, |panel, cx| {
4746        panel.project.update(cx, |_, cx| {
4747            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
4748        })
4749    });
4750    cx.run_until_parked();
4751    assert_eq!(
4752        visible_entries_as_strings(&panel, 0..20, cx),
4753        &[
4754            "v project_root",
4755            "    > .git",
4756            "    v dir_1",
4757            "        > gitignored_dir",
4758            "          file_1.py  <== selected  <== marked",
4759            "          file_2.py",
4760            "          file_3.py",
4761            "    > dir_2",
4762            "      .gitignore",
4763        ],
4764        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
4765    );
4766
4767    panel.update(cx, |panel, cx| {
4768        panel.project.update(cx, |_, cx| {
4769            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
4770        })
4771    });
4772    cx.run_until_parked();
4773    assert_eq!(
4774        visible_entries_as_strings(&panel, 0..20, cx),
4775        &[
4776            "v project_root",
4777            "    > .git",
4778            "    v dir_1",
4779            "        > gitignored_dir",
4780            "          file_1.py",
4781            "          file_2.py",
4782            "          file_3.py",
4783            "    v dir_2",
4784            "          file_1.py  <== selected  <== marked",
4785            "          file_2.py",
4786            "          file_3.py",
4787            "      .gitignore",
4788        ],
4789        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
4790    );
4791
4792    panel.update(cx, |panel, cx| {
4793        panel.project.update(cx, |_, cx| {
4794            cx.emit(project::Event::ActiveEntryChanged(Some(
4795                gitignored_dir_file,
4796            )))
4797        })
4798    });
4799    cx.run_until_parked();
4800    assert_eq!(
4801        visible_entries_as_strings(&panel, 0..20, cx),
4802        &[
4803            "v project_root",
4804            "    > .git",
4805            "    v dir_1",
4806            "        > gitignored_dir",
4807            "          file_1.py",
4808            "          file_2.py",
4809            "          file_3.py",
4810            "    v dir_2",
4811            "          file_1.py  <== selected  <== marked",
4812            "          file_2.py",
4813            "          file_3.py",
4814            "      .gitignore",
4815        ],
4816        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
4817    );
4818
4819    panel.update(cx, |panel, cx| {
4820        panel.project.update(cx, |_, cx| {
4821            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4822        })
4823    });
4824    cx.run_until_parked();
4825    assert_eq!(
4826        visible_entries_as_strings(&panel, 0..20, cx),
4827        &[
4828            "v project_root",
4829            "    > .git",
4830            "    v dir_1",
4831            "        v gitignored_dir",
4832            "              file_a.py  <== selected  <== marked",
4833            "              file_b.py",
4834            "              file_c.py",
4835            "          file_1.py",
4836            "          file_2.py",
4837            "          file_3.py",
4838            "    v dir_2",
4839            "          file_1.py",
4840            "          file_2.py",
4841            "          file_3.py",
4842            "      .gitignore",
4843        ],
4844        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
4845    );
4846
4847    panel.update(cx, |panel, cx| {
4848        panel.project.update(cx, |_, cx| {
4849            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
4850        })
4851    });
4852    cx.run_until_parked();
4853    assert_eq!(
4854        visible_entries_as_strings(&panel, 0..20, cx),
4855        &[
4856            "v project_root",
4857            "    > .git",
4858            "    v dir_1",
4859            "        v gitignored_dir",
4860            "              file_a.py",
4861            "              file_b.py",
4862            "              file_c.py",
4863            "          file_1.py",
4864            "          file_2.py",
4865            "          file_3.py",
4866            "    v dir_2",
4867            "          file_1.py  <== selected  <== marked",
4868            "          file_2.py",
4869            "          file_3.py",
4870            "      .gitignore",
4871        ],
4872        "After switching to dir_2_file, it should be selected and marked"
4873    );
4874
4875    panel.update(cx, |panel, cx| {
4876        panel.project.update(cx, |_, cx| {
4877            cx.emit(project::Event::ActiveEntryChanged(Some(
4878                gitignored_dir_file,
4879            )))
4880        })
4881    });
4882    cx.run_until_parked();
4883    assert_eq!(
4884        visible_entries_as_strings(&panel, 0..20, cx),
4885        &[
4886            "v project_root",
4887            "    > .git",
4888            "    v dir_1",
4889            "        v gitignored_dir",
4890            "              file_a.py  <== selected  <== marked",
4891            "              file_b.py",
4892            "              file_c.py",
4893            "          file_1.py",
4894            "          file_2.py",
4895            "          file_3.py",
4896            "    v dir_2",
4897            "          file_1.py",
4898            "          file_2.py",
4899            "          file_3.py",
4900            "      .gitignore",
4901        ],
4902        "When a gitignored entry is already visible, auto reveal should mark it as selected"
4903    );
4904}
4905
4906#[gpui::test]
4907async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
4908    init_test_with_editor(cx);
4909    cx.update(|cx| {
4910        cx.update_global::<SettingsStore, _>(|store, cx| {
4911            store.update_user_settings(cx, |settings| {
4912                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4913                settings.project.worktree.file_scan_inclusions =
4914                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
4915                settings
4916                    .project_panel
4917                    .get_or_insert_default()
4918                    .auto_reveal_entries = Some(false)
4919            });
4920        })
4921    });
4922
4923    let fs = FakeFs::new(cx.background_executor.clone());
4924    fs.insert_tree(
4925        "/project_root",
4926        json!({
4927            ".git": {},
4928            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
4929            "dir_1": {
4930                "file_1.py": "# File 1_1 contents",
4931                "file_2.py": "# File 1_2 contents",
4932                "file_3.py": "# File 1_3 contents",
4933                "gitignored_dir": {
4934                    "file_a.py": "# File contents",
4935                    "file_b.py": "# File contents",
4936                    "file_c.py": "# File contents",
4937                },
4938            },
4939            "dir_2": {
4940                "file_1.py": "# File 2_1 contents",
4941                "file_2.py": "# File 2_2 contents",
4942                "file_3.py": "# File 2_3 contents",
4943            },
4944            "always_included_but_ignored_dir": {
4945                "file_a.py": "# File contents",
4946                "file_b.py": "# File contents",
4947                "file_c.py": "# File contents",
4948            },
4949        }),
4950    )
4951    .await;
4952
4953    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4954    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4955    let workspace = window
4956        .read_with(cx, |mw, _| mw.workspace().clone())
4957        .unwrap();
4958    let cx = &mut VisualTestContext::from_window(window.into(), cx);
4959    let panel = workspace.update_in(cx, ProjectPanel::new);
4960    cx.run_until_parked();
4961
4962    assert_eq!(
4963        visible_entries_as_strings(&panel, 0..20, cx),
4964        &[
4965            "v project_root",
4966            "    > .git",
4967            "    > always_included_but_ignored_dir",
4968            "    > dir_1",
4969            "    > dir_2",
4970            "      .gitignore",
4971        ]
4972    );
4973
4974    let gitignored_dir_file =
4975        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4976    let always_included_but_ignored_dir_file = find_project_entry(
4977        &panel,
4978        "project_root/always_included_but_ignored_dir/file_a.py",
4979        cx,
4980    )
4981    .expect("file that is .gitignored but set to always be included should have an entry");
4982    assert_eq!(
4983        gitignored_dir_file, None,
4984        "File in the gitignored dir should not have an entry unless its directory is toggled"
4985    );
4986
4987    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4988    cx.run_until_parked();
4989    cx.update(|_, cx| {
4990        cx.update_global::<SettingsStore, _>(|store, cx| {
4991            store.update_user_settings(cx, |settings| {
4992                settings
4993                    .project_panel
4994                    .get_or_insert_default()
4995                    .auto_reveal_entries = Some(true)
4996            });
4997        })
4998    });
4999
5000    panel.update(cx, |panel, cx| {
5001        panel.project.update(cx, |_, cx| {
5002            cx.emit(project::Event::ActiveEntryChanged(Some(
5003                always_included_but_ignored_dir_file,
5004            )))
5005        })
5006    });
5007    cx.run_until_parked();
5008
5009    assert_eq!(
5010        visible_entries_as_strings(&panel, 0..20, cx),
5011        &[
5012            "v project_root",
5013            "    > .git",
5014            "    v always_included_but_ignored_dir",
5015            "          file_a.py  <== selected  <== marked",
5016            "          file_b.py",
5017            "          file_c.py",
5018            "    v dir_1",
5019            "        > gitignored_dir",
5020            "          file_1.py",
5021            "          file_2.py",
5022            "          file_3.py",
5023            "    > dir_2",
5024            "      .gitignore",
5025        ],
5026        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
5027    );
5028}
5029
5030#[gpui::test]
5031async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
5032    init_test_with_editor(cx);
5033    cx.update(|cx| {
5034        cx.update_global::<SettingsStore, _>(|store, cx| {
5035            store.update_user_settings(cx, |settings| {
5036                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
5037                settings
5038                    .project_panel
5039                    .get_or_insert_default()
5040                    .auto_reveal_entries = Some(false)
5041            });
5042        })
5043    });
5044
5045    let fs = FakeFs::new(cx.background_executor.clone());
5046    fs.insert_tree(
5047        "/project_root",
5048        json!({
5049            ".git": {},
5050            ".gitignore": "**/gitignored_dir",
5051            "dir_1": {
5052                "file_1.py": "# File 1_1 contents",
5053                "file_2.py": "# File 1_2 contents",
5054                "file_3.py": "# File 1_3 contents",
5055                "gitignored_dir": {
5056                    "file_a.py": "# File contents",
5057                    "file_b.py": "# File contents",
5058                    "file_c.py": "# File contents",
5059                },
5060            },
5061            "dir_2": {
5062                "file_1.py": "# File 2_1 contents",
5063                "file_2.py": "# File 2_2 contents",
5064                "file_3.py": "# File 2_3 contents",
5065            }
5066        }),
5067    )
5068    .await;
5069
5070    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5071    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5072    let workspace = window
5073        .read_with(cx, |mw, _| mw.workspace().clone())
5074        .unwrap();
5075    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5076    let panel = workspace.update_in(cx, ProjectPanel::new);
5077    cx.run_until_parked();
5078
5079    assert_eq!(
5080        visible_entries_as_strings(&panel, 0..20, cx),
5081        &[
5082            "v project_root",
5083            "    > .git",
5084            "    > dir_1",
5085            "    > dir_2",
5086            "      .gitignore",
5087        ]
5088    );
5089
5090    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
5091        .expect("dir 1 file is not ignored and should have an entry");
5092    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
5093        .expect("dir 2 file is not ignored and should have an entry");
5094    let gitignored_dir_file =
5095        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
5096    assert_eq!(
5097        gitignored_dir_file, None,
5098        "File in the gitignored dir should not have an entry before its dir is toggled"
5099    );
5100
5101    toggle_expand_dir(&panel, "project_root/dir_1", cx);
5102    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5103    cx.run_until_parked();
5104    assert_eq!(
5105        visible_entries_as_strings(&panel, 0..20, cx),
5106        &[
5107            "v project_root",
5108            "    > .git",
5109            "    v dir_1",
5110            "        v gitignored_dir  <== selected",
5111            "              file_a.py",
5112            "              file_b.py",
5113            "              file_c.py",
5114            "          file_1.py",
5115            "          file_2.py",
5116            "          file_3.py",
5117            "    > dir_2",
5118            "      .gitignore",
5119        ],
5120        "Should show gitignored dir file list in the project panel"
5121    );
5122    let gitignored_dir_file =
5123        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
5124            .expect("after gitignored dir got opened, a file entry should be present");
5125
5126    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5127    toggle_expand_dir(&panel, "project_root/dir_1", cx);
5128    assert_eq!(
5129        visible_entries_as_strings(&panel, 0..20, cx),
5130        &[
5131            "v project_root",
5132            "    > .git",
5133            "    > dir_1  <== selected",
5134            "    > dir_2",
5135            "      .gitignore",
5136        ],
5137        "Should hide all dir contents again and prepare for the explicit reveal test"
5138    );
5139
5140    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
5141        panel.update(cx, |panel, cx| {
5142            panel.project.update(cx, |_, cx| {
5143                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
5144            })
5145        });
5146        cx.run_until_parked();
5147        assert_eq!(
5148            visible_entries_as_strings(&panel, 0..20, cx),
5149            &[
5150                "v project_root",
5151                "    > .git",
5152                "    > dir_1  <== selected",
5153                "    > dir_2",
5154                "      .gitignore",
5155            ],
5156            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
5157        );
5158    }
5159
5160    panel.update(cx, |panel, cx| {
5161        panel.project.update(cx, |_, cx| {
5162            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
5163        })
5164    });
5165    cx.run_until_parked();
5166    assert_eq!(
5167        visible_entries_as_strings(&panel, 0..20, cx),
5168        &[
5169            "v project_root",
5170            "    > .git",
5171            "    v dir_1",
5172            "        > gitignored_dir",
5173            "          file_1.py  <== selected  <== marked",
5174            "          file_2.py",
5175            "          file_3.py",
5176            "    > dir_2",
5177            "      .gitignore",
5178        ],
5179        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
5180    );
5181
5182    panel.update(cx, |panel, cx| {
5183        panel.project.update(cx, |_, cx| {
5184            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
5185        })
5186    });
5187    cx.run_until_parked();
5188    assert_eq!(
5189        visible_entries_as_strings(&panel, 0..20, cx),
5190        &[
5191            "v project_root",
5192            "    > .git",
5193            "    v dir_1",
5194            "        > gitignored_dir",
5195            "          file_1.py",
5196            "          file_2.py",
5197            "          file_3.py",
5198            "    v dir_2",
5199            "          file_1.py  <== selected  <== marked",
5200            "          file_2.py",
5201            "          file_3.py",
5202            "      .gitignore",
5203        ],
5204        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
5205    );
5206
5207    panel.update(cx, |panel, cx| {
5208        panel.project.update(cx, |_, cx| {
5209            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
5210        })
5211    });
5212    cx.run_until_parked();
5213    assert_eq!(
5214        visible_entries_as_strings(&panel, 0..20, cx),
5215        &[
5216            "v project_root",
5217            "    > .git",
5218            "    v dir_1",
5219            "        v gitignored_dir",
5220            "              file_a.py  <== selected  <== marked",
5221            "              file_b.py",
5222            "              file_c.py",
5223            "          file_1.py",
5224            "          file_2.py",
5225            "          file_3.py",
5226            "    v dir_2",
5227            "          file_1.py",
5228            "          file_2.py",
5229            "          file_3.py",
5230            "      .gitignore",
5231        ],
5232        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
5233    );
5234}
5235
5236#[gpui::test]
5237async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
5238    init_test(cx);
5239    cx.update(|cx| {
5240        cx.update_global::<SettingsStore, _>(|store, cx| {
5241            store.update_user_settings(cx, |settings| {
5242                settings.project.worktree.file_scan_exclusions =
5243                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
5244            });
5245        });
5246    });
5247
5248    cx.update(|cx| {
5249        register_project_item::<TestProjectItemView>(cx);
5250    });
5251
5252    let fs = FakeFs::new(cx.executor());
5253    fs.insert_tree(
5254        "/root1",
5255        json!({
5256            ".dockerignore": "",
5257            ".git": {
5258                "HEAD": "",
5259            },
5260        }),
5261    )
5262    .await;
5263
5264    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
5265    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5266    let workspace = window
5267        .read_with(cx, |mw, _| mw.workspace().clone())
5268        .unwrap();
5269    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5270    let panel = workspace.update_in(cx, |workspace, window, cx| {
5271        let panel = ProjectPanel::new(workspace, window, cx);
5272        workspace.add_panel(panel.clone(), window, cx);
5273        panel
5274    });
5275    cx.run_until_parked();
5276
5277    select_path(&panel, "root1", cx);
5278    assert_eq!(
5279        visible_entries_as_strings(&panel, 0..10, cx),
5280        &["v root1  <== selected", "      .dockerignore",]
5281    );
5282    workspace.update_in(cx, |workspace, _, cx| {
5283        assert!(
5284            workspace.active_item(cx).is_none(),
5285            "Should have no active items in the beginning"
5286        );
5287    });
5288
5289    let excluded_file_path = ".git/COMMIT_EDITMSG";
5290    let excluded_dir_path = "excluded_dir";
5291
5292    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
5293    cx.run_until_parked();
5294    panel.update_in(cx, |panel, window, cx| {
5295        assert!(panel.filename_editor.read(cx).is_focused(window));
5296    });
5297    panel
5298        .update_in(cx, |panel, window, cx| {
5299            panel.filename_editor.update(cx, |editor, cx| {
5300                editor.set_text(excluded_file_path, window, cx)
5301            });
5302            panel.confirm_edit(true, window, cx).unwrap()
5303        })
5304        .await
5305        .unwrap();
5306
5307    assert_eq!(
5308        visible_entries_as_strings(&panel, 0..13, cx),
5309        &["v root1", "      .dockerignore"],
5310        "Excluded dir should not be shown after opening a file in it"
5311    );
5312    panel.update_in(cx, |panel, window, cx| {
5313        assert!(
5314            !panel.filename_editor.read(cx).is_focused(window),
5315            "Should have closed the file name editor"
5316        );
5317    });
5318    workspace.update_in(cx, |workspace, _, cx| {
5319        let active_entry_path = workspace
5320            .active_item(cx)
5321            .expect("should have opened and activated the excluded item")
5322            .act_as::<TestProjectItemView>(cx)
5323            .expect("should have opened the corresponding project item for the excluded item")
5324            .read(cx)
5325            .path
5326            .clone();
5327        assert_eq!(
5328            active_entry_path.path.as_ref(),
5329            rel_path(excluded_file_path),
5330            "Should open the excluded file"
5331        );
5332
5333        assert!(
5334            workspace.notification_ids().is_empty(),
5335            "Should have no notifications after opening an excluded file"
5336        );
5337    });
5338    assert!(
5339        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
5340        "Should have created the excluded file"
5341    );
5342
5343    select_path(&panel, "root1", cx);
5344    panel.update_in(cx, |panel, window, cx| {
5345        panel.new_directory(&NewDirectory, window, cx)
5346    });
5347    cx.run_until_parked();
5348    panel.update_in(cx, |panel, window, cx| {
5349        assert!(panel.filename_editor.read(cx).is_focused(window));
5350    });
5351    panel
5352        .update_in(cx, |panel, window, cx| {
5353            panel.filename_editor.update(cx, |editor, cx| {
5354                editor.set_text(excluded_file_path, window, cx)
5355            });
5356            panel.confirm_edit(true, window, cx).unwrap()
5357        })
5358        .await
5359        .unwrap();
5360    cx.run_until_parked();
5361    assert_eq!(
5362        visible_entries_as_strings(&panel, 0..13, cx),
5363        &["v root1", "      .dockerignore"],
5364        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
5365    );
5366    panel.update_in(cx, |panel, window, cx| {
5367        assert!(
5368            !panel.filename_editor.read(cx).is_focused(window),
5369            "Should have closed the file name editor"
5370        );
5371    });
5372    workspace.update_in(cx, |workspace, _, cx| {
5373        let notifications = workspace.notification_ids();
5374        assert_eq!(
5375            notifications.len(),
5376            1,
5377            "Should receive one notification with the error message"
5378        );
5379        workspace.dismiss_notification(notifications.first().unwrap(), cx);
5380        assert!(workspace.notification_ids().is_empty());
5381    });
5382
5383    select_path(&panel, "root1", cx);
5384    panel.update_in(cx, |panel, window, cx| {
5385        panel.new_directory(&NewDirectory, window, cx)
5386    });
5387    cx.run_until_parked();
5388
5389    panel.update_in(cx, |panel, window, cx| {
5390        assert!(panel.filename_editor.read(cx).is_focused(window));
5391    });
5392
5393    panel
5394        .update_in(cx, |panel, window, cx| {
5395            panel.filename_editor.update(cx, |editor, cx| {
5396                editor.set_text(excluded_dir_path, window, cx)
5397            });
5398            panel.confirm_edit(true, window, cx).unwrap()
5399        })
5400        .await
5401        .unwrap();
5402
5403    cx.run_until_parked();
5404
5405    assert_eq!(
5406        visible_entries_as_strings(&panel, 0..13, cx),
5407        &["v root1", "      .dockerignore"],
5408        "Should not change the project panel after trying to create an excluded directory"
5409    );
5410    panel.update_in(cx, |panel, window, cx| {
5411        assert!(
5412            !panel.filename_editor.read(cx).is_focused(window),
5413            "Should have closed the file name editor"
5414        );
5415    });
5416    workspace.update_in(cx, |workspace, _, cx| {
5417        let notifications = workspace.notification_ids();
5418        assert_eq!(
5419            notifications.len(),
5420            1,
5421            "Should receive one notification explaining that no directory is actually shown"
5422        );
5423        workspace.dismiss_notification(notifications.first().unwrap(), cx);
5424        assert!(workspace.notification_ids().is_empty());
5425    });
5426    assert!(
5427        fs.is_dir(Path::new("/root1/excluded_dir")).await,
5428        "Should have created the excluded directory"
5429    );
5430}
5431
5432#[gpui::test]
5433async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
5434    init_test_with_editor(cx);
5435
5436    let fs = FakeFs::new(cx.executor());
5437    fs.insert_tree(
5438        "/src",
5439        json!({
5440            "test": {
5441                "first.rs": "// First Rust file",
5442                "second.rs": "// Second Rust file",
5443                "third.rs": "// Third Rust file",
5444            }
5445        }),
5446    )
5447    .await;
5448
5449    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
5450    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5451    let workspace = window
5452        .read_with(cx, |mw, _| mw.workspace().clone())
5453        .unwrap();
5454    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5455    let panel = workspace.update_in(cx, |workspace, window, cx| {
5456        let panel = ProjectPanel::new(workspace, window, cx);
5457        workspace.add_panel(panel.clone(), window, cx);
5458        panel
5459    });
5460    cx.run_until_parked();
5461
5462    select_path(&panel, "src", cx);
5463    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
5464    cx.executor().run_until_parked();
5465    assert_eq!(
5466        visible_entries_as_strings(&panel, 0..10, cx),
5467        &[
5468            //
5469            "v src  <== selected",
5470            "    > test"
5471        ]
5472    );
5473    panel.update_in(cx, |panel, window, cx| {
5474        panel.new_directory(&NewDirectory, window, cx)
5475    });
5476    cx.executor().run_until_parked();
5477    panel.update_in(cx, |panel, window, cx| {
5478        assert!(panel.filename_editor.read(cx).is_focused(window));
5479    });
5480    assert_eq!(
5481        visible_entries_as_strings(&panel, 0..10, cx),
5482        &[
5483            //
5484            "v src",
5485            "    > [EDITOR: '']  <== selected",
5486            "    > test"
5487        ]
5488    );
5489
5490    panel.update_in(cx, |panel, window, cx| {
5491        panel.cancel(&menu::Cancel, window, cx);
5492    });
5493    cx.executor().run_until_parked();
5494    assert_eq!(
5495        visible_entries_as_strings(&panel, 0..10, cx),
5496        &[
5497            //
5498            "v src  <== selected",
5499            "    > test"
5500        ]
5501    );
5502
5503    panel.update_in(cx, |panel, window, cx| {
5504        panel.new_directory(&NewDirectory, window, cx)
5505    });
5506    cx.executor().run_until_parked();
5507    panel.update_in(cx, |panel, window, cx| {
5508        assert!(panel.filename_editor.read(cx).is_focused(window));
5509    });
5510    assert_eq!(
5511        visible_entries_as_strings(&panel, 0..10, cx),
5512        &[
5513            //
5514            "v src",
5515            "    > [EDITOR: '']  <== selected",
5516            "    > test"
5517        ]
5518    );
5519    workspace.update_in(cx, |_, window, _| window.blur());
5520    cx.executor().run_until_parked();
5521    assert_eq!(
5522        visible_entries_as_strings(&panel, 0..10, cx),
5523        &[
5524            //
5525            "v src  <== selected",
5526            "    > test"
5527        ]
5528    );
5529}
5530
5531#[gpui::test]
5532async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
5533    init_test_with_editor(cx);
5534
5535    let fs = FakeFs::new(cx.executor());
5536    fs.insert_tree(
5537        "/root",
5538        json!({
5539            "dir1": {
5540                "subdir1": {},
5541                "file1.txt": "",
5542                "file2.txt": "",
5543            },
5544            "dir2": {
5545                "subdir2": {},
5546                "file3.txt": "",
5547                "file4.txt": "",
5548            },
5549            "file5.txt": "",
5550            "file6.txt": "",
5551        }),
5552    )
5553    .await;
5554
5555    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5556    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5557    let workspace = window
5558        .read_with(cx, |mw, _| mw.workspace().clone())
5559        .unwrap();
5560    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5561    let panel = workspace.update_in(cx, ProjectPanel::new);
5562    cx.run_until_parked();
5563
5564    toggle_expand_dir(&panel, "root/dir1", cx);
5565    toggle_expand_dir(&panel, "root/dir2", cx);
5566
5567    // Test Case 1: Delete middle file in directory
5568    select_path(&panel, "root/dir1/file1.txt", cx);
5569    assert_eq!(
5570        visible_entries_as_strings(&panel, 0..15, cx),
5571        &[
5572            "v root",
5573            "    v dir1",
5574            "        > subdir1",
5575            "          file1.txt  <== selected",
5576            "          file2.txt",
5577            "    v dir2",
5578            "        > subdir2",
5579            "          file3.txt",
5580            "          file4.txt",
5581            "      file5.txt",
5582            "      file6.txt",
5583        ],
5584        "Initial state before deleting middle file"
5585    );
5586
5587    submit_deletion(&panel, cx);
5588    assert_eq!(
5589        visible_entries_as_strings(&panel, 0..15, cx),
5590        &[
5591            "v root",
5592            "    v dir1",
5593            "        > subdir1",
5594            "          file2.txt  <== selected",
5595            "    v dir2",
5596            "        > subdir2",
5597            "          file3.txt",
5598            "          file4.txt",
5599            "      file5.txt",
5600            "      file6.txt",
5601        ],
5602        "Should select next file after deleting middle file"
5603    );
5604
5605    // Test Case 2: Delete last file in directory
5606    submit_deletion(&panel, cx);
5607    assert_eq!(
5608        visible_entries_as_strings(&panel, 0..15, cx),
5609        &[
5610            "v root",
5611            "    v dir1",
5612            "        > subdir1  <== selected",
5613            "    v dir2",
5614            "        > subdir2",
5615            "          file3.txt",
5616            "          file4.txt",
5617            "      file5.txt",
5618            "      file6.txt",
5619        ],
5620        "Should select next directory when last file is deleted"
5621    );
5622
5623    // Test Case 3: Delete root level file
5624    select_path(&panel, "root/file6.txt", cx);
5625    assert_eq!(
5626        visible_entries_as_strings(&panel, 0..15, cx),
5627        &[
5628            "v root",
5629            "    v dir1",
5630            "        > subdir1",
5631            "    v dir2",
5632            "        > subdir2",
5633            "          file3.txt",
5634            "          file4.txt",
5635            "      file5.txt",
5636            "      file6.txt  <== selected",
5637        ],
5638        "Initial state before deleting root level file"
5639    );
5640
5641    submit_deletion(&panel, cx);
5642    assert_eq!(
5643        visible_entries_as_strings(&panel, 0..15, cx),
5644        &[
5645            "v root",
5646            "    v dir1",
5647            "        > subdir1",
5648            "    v dir2",
5649            "        > subdir2",
5650            "          file3.txt",
5651            "          file4.txt",
5652            "      file5.txt  <== selected",
5653        ],
5654        "Should select prev entry at root level"
5655    );
5656}
5657
5658#[gpui::test]
5659async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
5660    init_test_with_editor(cx);
5661
5662    let fs = FakeFs::new(cx.executor());
5663    fs.insert_tree(
5664        path!("/root"),
5665        json!({
5666            "aa": "// Testing 1",
5667            "bb": "// Testing 2",
5668            "cc": "// Testing 3",
5669            "dd": "// Testing 4",
5670            "ee": "// Testing 5",
5671            "ff": "// Testing 6",
5672            "gg": "// Testing 7",
5673            "hh": "// Testing 8",
5674            "ii": "// Testing 8",
5675            ".gitignore": "bb\ndd\nee\nff\nii\n'",
5676        }),
5677    )
5678    .await;
5679
5680    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5681    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5682    let workspace = window
5683        .read_with(cx, |mw, _| mw.workspace().clone())
5684        .unwrap();
5685    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5686
5687    // Test 1: Auto selection with one gitignored file next to the deleted file
5688    cx.update(|_, cx| {
5689        let settings = *ProjectPanelSettings::get_global(cx);
5690        ProjectPanelSettings::override_global(
5691            ProjectPanelSettings {
5692                hide_gitignore: true,
5693                ..settings
5694            },
5695            cx,
5696        );
5697    });
5698
5699    let panel = workspace.update_in(cx, ProjectPanel::new);
5700    cx.run_until_parked();
5701
5702    select_path(&panel, "root/aa", cx);
5703    assert_eq!(
5704        visible_entries_as_strings(&panel, 0..10, cx),
5705        &[
5706            "v root",
5707            "      .gitignore",
5708            "      aa  <== selected",
5709            "      cc",
5710            "      gg",
5711            "      hh"
5712        ],
5713        "Initial state should hide files on .gitignore"
5714    );
5715
5716    submit_deletion(&panel, cx);
5717
5718    assert_eq!(
5719        visible_entries_as_strings(&panel, 0..10, cx),
5720        &[
5721            "v root",
5722            "      .gitignore",
5723            "      cc  <== selected",
5724            "      gg",
5725            "      hh"
5726        ],
5727        "Should select next entry not on .gitignore"
5728    );
5729
5730    // Test 2: Auto selection with many gitignored files next to the deleted file
5731    submit_deletion(&panel, cx);
5732    assert_eq!(
5733        visible_entries_as_strings(&panel, 0..10, cx),
5734        &[
5735            "v root",
5736            "      .gitignore",
5737            "      gg  <== selected",
5738            "      hh"
5739        ],
5740        "Should select next entry not on .gitignore"
5741    );
5742
5743    // Test 3: Auto selection of entry before deleted file
5744    select_path(&panel, "root/hh", cx);
5745    assert_eq!(
5746        visible_entries_as_strings(&panel, 0..10, cx),
5747        &[
5748            "v root",
5749            "      .gitignore",
5750            "      gg",
5751            "      hh  <== selected"
5752        ],
5753        "Should select next entry not on .gitignore"
5754    );
5755    submit_deletion(&panel, cx);
5756    assert_eq!(
5757        visible_entries_as_strings(&panel, 0..10, cx),
5758        &["v root", "      .gitignore", "      gg  <== selected"],
5759        "Should select next entry not on .gitignore"
5760    );
5761}
5762
5763#[gpui::test]
5764async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
5765    init_test_with_editor(cx);
5766
5767    let fs = FakeFs::new(cx.executor());
5768    fs.insert_tree(
5769        path!("/root"),
5770        json!({
5771            "dir1": {
5772                "file1": "// Testing",
5773                "file2": "// Testing",
5774                "file3": "// Testing"
5775            },
5776            "aa": "// Testing",
5777            ".gitignore": "file1\nfile3\n",
5778        }),
5779    )
5780    .await;
5781
5782    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5783    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5784    let workspace = window
5785        .read_with(cx, |mw, _| mw.workspace().clone())
5786        .unwrap();
5787    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5788
5789    cx.update(|_, cx| {
5790        let settings = *ProjectPanelSettings::get_global(cx);
5791        ProjectPanelSettings::override_global(
5792            ProjectPanelSettings {
5793                hide_gitignore: true,
5794                ..settings
5795            },
5796            cx,
5797        );
5798    });
5799
5800    let panel = workspace.update_in(cx, ProjectPanel::new);
5801    cx.run_until_parked();
5802
5803    // Test 1: Visible items should exclude files on gitignore
5804    toggle_expand_dir(&panel, "root/dir1", cx);
5805    select_path(&panel, "root/dir1/file2", cx);
5806    assert_eq!(
5807        visible_entries_as_strings(&panel, 0..10, cx),
5808        &[
5809            "v root",
5810            "    v dir1",
5811            "          file2  <== selected",
5812            "      .gitignore",
5813            "      aa"
5814        ],
5815        "Initial state should hide files on .gitignore"
5816    );
5817    submit_deletion(&panel, cx);
5818
5819    // Test 2: Auto selection should go to the parent
5820    assert_eq!(
5821        visible_entries_as_strings(&panel, 0..10, cx),
5822        &[
5823            "v root",
5824            "    v dir1  <== selected",
5825            "      .gitignore",
5826            "      aa"
5827        ],
5828        "Initial state should hide files on .gitignore"
5829    );
5830}
5831
5832#[gpui::test]
5833async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
5834    init_test_with_editor(cx);
5835
5836    let fs = FakeFs::new(cx.executor());
5837    fs.insert_tree(
5838        "/root",
5839        json!({
5840            "dir1": {
5841                "subdir1": {
5842                    "a.txt": "",
5843                    "b.txt": ""
5844                },
5845                "file1.txt": "",
5846            },
5847            "dir2": {
5848                "subdir2": {
5849                    "c.txt": "",
5850                    "d.txt": ""
5851                },
5852                "file2.txt": "",
5853            },
5854            "file3.txt": "",
5855        }),
5856    )
5857    .await;
5858
5859    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5860    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5861    let workspace = window
5862        .read_with(cx, |mw, _| mw.workspace().clone())
5863        .unwrap();
5864    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5865    let panel = workspace.update_in(cx, ProjectPanel::new);
5866    cx.run_until_parked();
5867
5868    toggle_expand_dir(&panel, "root/dir1", cx);
5869    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5870    toggle_expand_dir(&panel, "root/dir2", cx);
5871    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5872
5873    // Test Case 1: Select and delete nested directory with parent
5874    cx.simulate_modifiers_change(gpui::Modifiers {
5875        control: true,
5876        ..Default::default()
5877    });
5878    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5879    select_path_with_mark(&panel, "root/dir1", cx);
5880
5881    assert_eq!(
5882        visible_entries_as_strings(&panel, 0..15, cx),
5883        &[
5884            "v root",
5885            "    v dir1  <== selected  <== marked",
5886            "        v subdir1  <== marked",
5887            "              a.txt",
5888            "              b.txt",
5889            "          file1.txt",
5890            "    v dir2",
5891            "        v subdir2",
5892            "              c.txt",
5893            "              d.txt",
5894            "          file2.txt",
5895            "      file3.txt",
5896        ],
5897        "Initial state before deleting nested directory with parent"
5898    );
5899
5900    submit_deletion(&panel, cx);
5901    assert_eq!(
5902        visible_entries_as_strings(&panel, 0..15, cx),
5903        &[
5904            "v root",
5905            "    v dir2  <== selected",
5906            "        v subdir2",
5907            "              c.txt",
5908            "              d.txt",
5909            "          file2.txt",
5910            "      file3.txt",
5911        ],
5912        "Should select next directory after deleting directory with parent"
5913    );
5914
5915    // Test Case 2: Select mixed files and directories across levels
5916    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
5917    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
5918    select_path_with_mark(&panel, "root/file3.txt", cx);
5919
5920    assert_eq!(
5921        visible_entries_as_strings(&panel, 0..15, cx),
5922        &[
5923            "v root",
5924            "    v dir2",
5925            "        v subdir2",
5926            "              c.txt  <== marked",
5927            "              d.txt",
5928            "          file2.txt  <== marked",
5929            "      file3.txt  <== selected  <== marked",
5930        ],
5931        "Initial state before deleting"
5932    );
5933
5934    submit_deletion(&panel, cx);
5935    assert_eq!(
5936        visible_entries_as_strings(&panel, 0..15, cx),
5937        &[
5938            "v root",
5939            "    v dir2  <== selected",
5940            "        v subdir2",
5941            "              d.txt",
5942        ],
5943        "Should select sibling directory"
5944    );
5945}
5946
5947#[gpui::test]
5948async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
5949    init_test_with_editor(cx);
5950
5951    let fs = FakeFs::new(cx.executor());
5952    fs.insert_tree(
5953        "/root",
5954        json!({
5955            "dir1": {
5956                "subdir1": {
5957                    "a.txt": "",
5958                    "b.txt": ""
5959                },
5960                "file1.txt": "",
5961            },
5962            "dir2": {
5963                "subdir2": {
5964                    "c.txt": "",
5965                    "d.txt": ""
5966                },
5967                "file2.txt": "",
5968            },
5969            "file3.txt": "",
5970            "file4.txt": "",
5971        }),
5972    )
5973    .await;
5974
5975    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5976    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5977    let workspace = window
5978        .read_with(cx, |mw, _| mw.workspace().clone())
5979        .unwrap();
5980    let cx = &mut VisualTestContext::from_window(window.into(), cx);
5981    let panel = workspace.update_in(cx, ProjectPanel::new);
5982    cx.run_until_parked();
5983
5984    toggle_expand_dir(&panel, "root/dir1", cx);
5985    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5986    toggle_expand_dir(&panel, "root/dir2", cx);
5987    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5988
5989    // Test Case 1: Select all root files and directories
5990    cx.simulate_modifiers_change(gpui::Modifiers {
5991        control: true,
5992        ..Default::default()
5993    });
5994    select_path_with_mark(&panel, "root/dir1", cx);
5995    select_path_with_mark(&panel, "root/dir2", cx);
5996    select_path_with_mark(&panel, "root/file3.txt", cx);
5997    select_path_with_mark(&panel, "root/file4.txt", cx);
5998    assert_eq!(
5999        visible_entries_as_strings(&panel, 0..20, cx),
6000        &[
6001            "v root",
6002            "    v dir1  <== marked",
6003            "        v subdir1",
6004            "              a.txt",
6005            "              b.txt",
6006            "          file1.txt",
6007            "    v dir2  <== marked",
6008            "        v subdir2",
6009            "              c.txt",
6010            "              d.txt",
6011            "          file2.txt",
6012            "      file3.txt  <== marked",
6013            "      file4.txt  <== selected  <== marked",
6014        ],
6015        "State before deleting all contents"
6016    );
6017
6018    submit_deletion(&panel, cx);
6019    assert_eq!(
6020        visible_entries_as_strings(&panel, 0..20, cx),
6021        &["v root  <== selected"],
6022        "Only empty root directory should remain after deleting all contents"
6023    );
6024}
6025
6026#[gpui::test]
6027async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
6028    init_test_with_editor(cx);
6029
6030    let fs = FakeFs::new(cx.executor());
6031    fs.insert_tree(
6032        "/root",
6033        json!({
6034            "dir1": {
6035                "subdir1": {
6036                    "file_a.txt": "content a",
6037                    "file_b.txt": "content b",
6038                },
6039                "subdir2": {
6040                    "file_c.txt": "content c",
6041                },
6042                "file1.txt": "content 1",
6043            },
6044            "dir2": {
6045                "file2.txt": "content 2",
6046            },
6047        }),
6048    )
6049    .await;
6050
6051    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6052    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6053    let workspace = window
6054        .read_with(cx, |mw, _| mw.workspace().clone())
6055        .unwrap();
6056    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6057    let panel = workspace.update_in(cx, ProjectPanel::new);
6058    cx.run_until_parked();
6059
6060    toggle_expand_dir(&panel, "root/dir1", cx);
6061    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6062    toggle_expand_dir(&panel, "root/dir2", cx);
6063    cx.simulate_modifiers_change(gpui::Modifiers {
6064        control: true,
6065        ..Default::default()
6066    });
6067
6068    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
6069    select_path_with_mark(&panel, "root/dir1", cx);
6070    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
6071    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
6072
6073    assert_eq!(
6074        visible_entries_as_strings(&panel, 0..20, cx),
6075        &[
6076            "v root",
6077            "    v dir1  <== marked",
6078            "        v subdir1  <== marked",
6079            "              file_a.txt  <== selected  <== marked",
6080            "              file_b.txt",
6081            "        > subdir2",
6082            "          file1.txt",
6083            "    v dir2",
6084            "          file2.txt",
6085        ],
6086        "State with parent dir, subdir, and file selected"
6087    );
6088    submit_deletion(&panel, cx);
6089    assert_eq!(
6090        visible_entries_as_strings(&panel, 0..20, cx),
6091        &["v root", "    v dir2  <== selected", "          file2.txt",],
6092        "Only dir2 should remain after deletion"
6093    );
6094}
6095
6096#[gpui::test]
6097async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
6098    init_test_with_editor(cx);
6099
6100    let fs = FakeFs::new(cx.executor());
6101    // First worktree
6102    fs.insert_tree(
6103        "/root1",
6104        json!({
6105            "dir1": {
6106                "file1.txt": "content 1",
6107                "file2.txt": "content 2",
6108            },
6109            "dir2": {
6110                "file3.txt": "content 3",
6111            },
6112        }),
6113    )
6114    .await;
6115
6116    // Second worktree
6117    fs.insert_tree(
6118        "/root2",
6119        json!({
6120            "dir3": {
6121                "file4.txt": "content 4",
6122                "file5.txt": "content 5",
6123            },
6124            "file6.txt": "content 6",
6125        }),
6126    )
6127    .await;
6128
6129    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6130    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6131    let workspace = window
6132        .read_with(cx, |mw, _| mw.workspace().clone())
6133        .unwrap();
6134    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6135    let panel = workspace.update_in(cx, ProjectPanel::new);
6136    cx.run_until_parked();
6137
6138    // Expand all directories for testing
6139    toggle_expand_dir(&panel, "root1/dir1", cx);
6140    toggle_expand_dir(&panel, "root1/dir2", cx);
6141    toggle_expand_dir(&panel, "root2/dir3", cx);
6142
6143    // Test Case 1: Delete files across different worktrees
6144    cx.simulate_modifiers_change(gpui::Modifiers {
6145        control: true,
6146        ..Default::default()
6147    });
6148    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
6149    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
6150
6151    assert_eq!(
6152        visible_entries_as_strings(&panel, 0..20, cx),
6153        &[
6154            "v root1",
6155            "    v dir1",
6156            "          file1.txt  <== marked",
6157            "          file2.txt",
6158            "    v dir2",
6159            "          file3.txt",
6160            "v root2",
6161            "    v dir3",
6162            "          file4.txt  <== selected  <== marked",
6163            "          file5.txt",
6164            "      file6.txt",
6165        ],
6166        "Initial state with files selected from different worktrees"
6167    );
6168
6169    submit_deletion(&panel, cx);
6170    assert_eq!(
6171        visible_entries_as_strings(&panel, 0..20, cx),
6172        &[
6173            "v root1",
6174            "    v dir1",
6175            "          file2.txt",
6176            "    v dir2",
6177            "          file3.txt",
6178            "v root2",
6179            "    v dir3",
6180            "          file5.txt  <== selected",
6181            "      file6.txt",
6182        ],
6183        "Should select next file in the last worktree after deletion"
6184    );
6185
6186    // Test Case 2: Delete directories from different worktrees
6187    select_path_with_mark(&panel, "root1/dir1", cx);
6188    select_path_with_mark(&panel, "root2/dir3", cx);
6189
6190    assert_eq!(
6191        visible_entries_as_strings(&panel, 0..20, cx),
6192        &[
6193            "v root1",
6194            "    v dir1  <== marked",
6195            "          file2.txt",
6196            "    v dir2",
6197            "          file3.txt",
6198            "v root2",
6199            "    v dir3  <== selected  <== marked",
6200            "          file5.txt",
6201            "      file6.txt",
6202        ],
6203        "State with directories marked from different worktrees"
6204    );
6205
6206    submit_deletion(&panel, cx);
6207    assert_eq!(
6208        visible_entries_as_strings(&panel, 0..20, cx),
6209        &[
6210            "v root1",
6211            "    v dir2",
6212            "          file3.txt",
6213            "v root2",
6214            "      file6.txt  <== selected",
6215        ],
6216        "Should select remaining file in last worktree after directory deletion"
6217    );
6218
6219    // Test Case 4: Delete all remaining files except roots
6220    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
6221    select_path_with_mark(&panel, "root2/file6.txt", cx);
6222
6223    assert_eq!(
6224        visible_entries_as_strings(&panel, 0..20, cx),
6225        &[
6226            "v root1",
6227            "    v dir2",
6228            "          file3.txt  <== marked",
6229            "v root2",
6230            "      file6.txt  <== selected  <== marked",
6231        ],
6232        "State with all remaining files marked"
6233    );
6234
6235    submit_deletion(&panel, cx);
6236    assert_eq!(
6237        visible_entries_as_strings(&panel, 0..20, cx),
6238        &["v root1", "    v dir2", "v root2  <== selected"],
6239        "Second parent root should be selected after deleting"
6240    );
6241}
6242
6243#[gpui::test]
6244async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
6245    init_test_with_editor(cx);
6246
6247    let fs = FakeFs::new(cx.executor());
6248    fs.insert_tree(
6249        "/root",
6250        json!({
6251            "dir1": {
6252                "file1.txt": "",
6253                "file2.txt": "",
6254                "file3.txt": "",
6255            },
6256            "dir2": {
6257                "file4.txt": "",
6258                "file5.txt": "",
6259            },
6260        }),
6261    )
6262    .await;
6263
6264    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6265    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6266    let workspace = window
6267        .read_with(cx, |mw, _| mw.workspace().clone())
6268        .unwrap();
6269    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6270    let panel = workspace.update_in(cx, ProjectPanel::new);
6271    cx.run_until_parked();
6272
6273    toggle_expand_dir(&panel, "root/dir1", cx);
6274    toggle_expand_dir(&panel, "root/dir2", cx);
6275
6276    cx.simulate_modifiers_change(gpui::Modifiers {
6277        control: true,
6278        ..Default::default()
6279    });
6280
6281    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
6282    select_path(&panel, "root/dir1/file1.txt", cx);
6283
6284    assert_eq!(
6285        visible_entries_as_strings(&panel, 0..15, cx),
6286        &[
6287            "v root",
6288            "    v dir1",
6289            "          file1.txt  <== selected",
6290            "          file2.txt  <== marked",
6291            "          file3.txt",
6292            "    v dir2",
6293            "          file4.txt",
6294            "          file5.txt",
6295        ],
6296        "Initial state with one marked entry and different selection"
6297    );
6298
6299    // Delete should operate on the selected entry (file1.txt)
6300    submit_deletion(&panel, cx);
6301    assert_eq!(
6302        visible_entries_as_strings(&panel, 0..15, cx),
6303        &[
6304            "v root",
6305            "    v dir1",
6306            "          file2.txt  <== selected  <== marked",
6307            "          file3.txt",
6308            "    v dir2",
6309            "          file4.txt",
6310            "          file5.txt",
6311        ],
6312        "Should delete selected file, not marked file"
6313    );
6314
6315    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
6316    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
6317    select_path(&panel, "root/dir2/file5.txt", cx);
6318
6319    assert_eq!(
6320        visible_entries_as_strings(&panel, 0..15, cx),
6321        &[
6322            "v root",
6323            "    v dir1",
6324            "          file2.txt  <== marked",
6325            "          file3.txt  <== marked",
6326            "    v dir2",
6327            "          file4.txt  <== marked",
6328            "          file5.txt  <== selected",
6329        ],
6330        "Initial state with multiple marked entries and different selection"
6331    );
6332
6333    // Delete should operate on all marked entries, ignoring the selection
6334    submit_deletion(&panel, cx);
6335    assert_eq!(
6336        visible_entries_as_strings(&panel, 0..15, cx),
6337        &[
6338            "v root",
6339            "    v dir1",
6340            "    v dir2",
6341            "          file5.txt  <== selected",
6342        ],
6343        "Should delete all marked files, leaving only the selected file"
6344    );
6345}
6346
6347#[gpui::test]
6348async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
6349    init_test_with_editor(cx);
6350
6351    let fs = FakeFs::new(cx.executor());
6352    fs.insert_tree(
6353        "/root_b",
6354        json!({
6355            "dir1": {
6356                "file1.txt": "content 1",
6357                "file2.txt": "content 2",
6358            },
6359        }),
6360    )
6361    .await;
6362
6363    fs.insert_tree(
6364        "/root_c",
6365        json!({
6366            "dir2": {},
6367        }),
6368    )
6369    .await;
6370
6371    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
6372    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6373    let workspace = window
6374        .read_with(cx, |mw, _| mw.workspace().clone())
6375        .unwrap();
6376    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6377    let panel = workspace.update_in(cx, ProjectPanel::new);
6378    cx.run_until_parked();
6379
6380    toggle_expand_dir(&panel, "root_b/dir1", cx);
6381    toggle_expand_dir(&panel, "root_c/dir2", cx);
6382
6383    cx.simulate_modifiers_change(gpui::Modifiers {
6384        control: true,
6385        ..Default::default()
6386    });
6387    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
6388    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
6389
6390    assert_eq!(
6391        visible_entries_as_strings(&panel, 0..20, cx),
6392        &[
6393            "v root_b",
6394            "    v dir1",
6395            "          file1.txt  <== marked",
6396            "          file2.txt  <== selected  <== marked",
6397            "v root_c",
6398            "    v dir2",
6399        ],
6400        "Initial state with files marked in root_b"
6401    );
6402
6403    submit_deletion(&panel, cx);
6404    assert_eq!(
6405        visible_entries_as_strings(&panel, 0..20, cx),
6406        &[
6407            "v root_b",
6408            "    v dir1  <== selected",
6409            "v root_c",
6410            "    v dir2",
6411        ],
6412        "After deletion in root_b as it's last deletion, selection should be in root_b"
6413    );
6414
6415    select_path_with_mark(&panel, "root_c/dir2", cx);
6416
6417    submit_deletion(&panel, cx);
6418    assert_eq!(
6419        visible_entries_as_strings(&panel, 0..20, cx),
6420        &["v root_b", "    v dir1", "v root_c  <== selected",],
6421        "After deleting from root_c, it should remain in root_c"
6422    );
6423}
6424
6425fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
6426    let path = rel_path(path);
6427    panel.update_in(cx, |panel, window, cx| {
6428        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6429            let worktree = worktree.read(cx);
6430            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6431                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6432                panel.toggle_expanded(entry_id, window, cx);
6433                return;
6434            }
6435        }
6436        panic!("no worktree for path {:?}", path);
6437    });
6438    cx.run_until_parked();
6439}
6440
6441#[gpui::test]
6442async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
6443    init_test_with_editor(cx);
6444
6445    let fs = FakeFs::new(cx.executor());
6446    fs.insert_tree(
6447        path!("/root"),
6448        json!({
6449            ".gitignore": "**/ignored_dir\n**/ignored_nested",
6450            "dir1": {
6451                "empty1": {
6452                    "empty2": {
6453                        "empty3": {
6454                            "file.txt": ""
6455                        }
6456                    }
6457                },
6458                "subdir1": {
6459                    "file1.txt": "",
6460                    "file2.txt": "",
6461                    "ignored_nested": {
6462                        "ignored_file.txt": ""
6463                    }
6464                },
6465                "ignored_dir": {
6466                    "subdir": {
6467                        "deep_file.txt": ""
6468                    }
6469                }
6470            }
6471        }),
6472    )
6473    .await;
6474
6475    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6476    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6477    let workspace = window
6478        .read_with(cx, |mw, _| mw.workspace().clone())
6479        .unwrap();
6480    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6481
6482    // Test 1: When auto-fold is enabled
6483    cx.update(|_, cx| {
6484        let settings = *ProjectPanelSettings::get_global(cx);
6485        ProjectPanelSettings::override_global(
6486            ProjectPanelSettings {
6487                auto_fold_dirs: true,
6488                ..settings
6489            },
6490            cx,
6491        );
6492    });
6493
6494    let panel = workspace.update_in(cx, ProjectPanel::new);
6495    cx.run_until_parked();
6496
6497    assert_eq!(
6498        visible_entries_as_strings(&panel, 0..20, cx),
6499        &["v root", "    > dir1", "      .gitignore",],
6500        "Initial state should show collapsed root structure"
6501    );
6502
6503    toggle_expand_dir(&panel, "root/dir1", cx);
6504    assert_eq!(
6505        visible_entries_as_strings(&panel, 0..20, cx),
6506        &[
6507            "v root",
6508            "    v dir1  <== selected",
6509            "        > empty1/empty2/empty3",
6510            "        > ignored_dir",
6511            "        > subdir1",
6512            "      .gitignore",
6513        ],
6514        "Should show first level with auto-folded dirs and ignored dir visible"
6515    );
6516
6517    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6518    panel.update_in(cx, |panel, window, cx| {
6519        let project = panel.project.read(cx);
6520        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6521        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6522        panel.update_visible_entries(None, false, false, window, cx);
6523    });
6524    cx.run_until_parked();
6525
6526    assert_eq!(
6527        visible_entries_as_strings(&panel, 0..20, cx),
6528        &[
6529            "v root",
6530            "    v dir1  <== selected",
6531            "        v empty1",
6532            "            v empty2",
6533            "                v empty3",
6534            "                      file.txt",
6535            "        > ignored_dir",
6536            "        v subdir1",
6537            "            > ignored_nested",
6538            "              file1.txt",
6539            "              file2.txt",
6540            "      .gitignore",
6541        ],
6542        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
6543    );
6544
6545    // Test 2: When auto-fold is disabled
6546    cx.update(|_, cx| {
6547        let settings = *ProjectPanelSettings::get_global(cx);
6548        ProjectPanelSettings::override_global(
6549            ProjectPanelSettings {
6550                auto_fold_dirs: false,
6551                ..settings
6552            },
6553            cx,
6554        );
6555    });
6556
6557    panel.update_in(cx, |panel, window, cx| {
6558        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6559    });
6560
6561    toggle_expand_dir(&panel, "root/dir1", cx);
6562    assert_eq!(
6563        visible_entries_as_strings(&panel, 0..20, cx),
6564        &[
6565            "v root",
6566            "    v dir1  <== selected",
6567            "        > empty1",
6568            "        > ignored_dir",
6569            "        > subdir1",
6570            "      .gitignore",
6571        ],
6572        "With auto-fold disabled: should show all directories separately"
6573    );
6574
6575    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6576    panel.update_in(cx, |panel, window, cx| {
6577        let project = panel.project.read(cx);
6578        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6579        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6580        panel.update_visible_entries(None, false, false, window, cx);
6581    });
6582    cx.run_until_parked();
6583
6584    assert_eq!(
6585        visible_entries_as_strings(&panel, 0..20, cx),
6586        &[
6587            "v root",
6588            "    v dir1  <== selected",
6589            "        v empty1",
6590            "            v empty2",
6591            "                v empty3",
6592            "                      file.txt",
6593            "        > ignored_dir",
6594            "        v subdir1",
6595            "            > ignored_nested",
6596            "              file1.txt",
6597            "              file2.txt",
6598            "      .gitignore",
6599        ],
6600        "After expand_all without auto-fold: should expand all dirs normally, \
6601         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
6602    );
6603
6604    // Test 3: When explicitly called on ignored directory
6605    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
6606    panel.update_in(cx, |panel, window, cx| {
6607        let project = panel.project.read(cx);
6608        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6609        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
6610        panel.update_visible_entries(None, false, false, window, cx);
6611    });
6612    cx.run_until_parked();
6613
6614    assert_eq!(
6615        visible_entries_as_strings(&panel, 0..20, cx),
6616        &[
6617            "v root",
6618            "    v dir1  <== selected",
6619            "        v empty1",
6620            "            v empty2",
6621            "                v empty3",
6622            "                      file.txt",
6623            "        v ignored_dir",
6624            "            v subdir",
6625            "                  deep_file.txt",
6626            "        v subdir1",
6627            "            > ignored_nested",
6628            "              file1.txt",
6629            "              file2.txt",
6630            "      .gitignore",
6631        ],
6632        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
6633    );
6634}
6635
6636#[gpui::test]
6637async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
6638    init_test(cx);
6639
6640    let fs = FakeFs::new(cx.executor());
6641    fs.insert_tree(
6642        path!("/root"),
6643        json!({
6644            "dir1": {
6645                "subdir1": {
6646                    "nested1": {
6647                        "file1.txt": "",
6648                        "file2.txt": ""
6649                    },
6650                },
6651                "subdir2": {
6652                    "file4.txt": ""
6653                }
6654            },
6655            "dir2": {
6656                "single_file": {
6657                    "file5.txt": ""
6658                }
6659            }
6660        }),
6661    )
6662    .await;
6663
6664    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6665    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6666    let workspace = window
6667        .read_with(cx, |mw, _| mw.workspace().clone())
6668        .unwrap();
6669    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6670
6671    // Test 1: Basic collapsing
6672    {
6673        let panel = workspace.update_in(cx, ProjectPanel::new);
6674        cx.run_until_parked();
6675
6676        toggle_expand_dir(&panel, "root/dir1", cx);
6677        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6678        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6679        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
6680
6681        assert_eq!(
6682            visible_entries_as_strings(&panel, 0..20, cx),
6683            &[
6684                "v root",
6685                "    v dir1",
6686                "        v subdir1",
6687                "            v nested1",
6688                "                  file1.txt",
6689                "                  file2.txt",
6690                "        v subdir2  <== selected",
6691                "              file4.txt",
6692                "    > dir2",
6693            ],
6694            "Initial state with everything expanded"
6695        );
6696
6697        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6698        panel.update_in(cx, |panel, window, cx| {
6699            let project = panel.project.read(cx);
6700            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6701            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6702            panel.update_visible_entries(None, false, false, window, cx);
6703        });
6704        cx.run_until_parked();
6705
6706        assert_eq!(
6707            visible_entries_as_strings(&panel, 0..20, cx),
6708            &["v root", "    > dir1", "    > dir2",],
6709            "All subdirs under dir1 should be collapsed"
6710        );
6711    }
6712
6713    // Test 2: With auto-fold enabled
6714    {
6715        cx.update(|_, cx| {
6716            let settings = *ProjectPanelSettings::get_global(cx);
6717            ProjectPanelSettings::override_global(
6718                ProjectPanelSettings {
6719                    auto_fold_dirs: true,
6720                    ..settings
6721                },
6722                cx,
6723            );
6724        });
6725
6726        let panel = workspace.update_in(cx, ProjectPanel::new);
6727        cx.run_until_parked();
6728
6729        toggle_expand_dir(&panel, "root/dir1", cx);
6730        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6731        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6732
6733        assert_eq!(
6734            visible_entries_as_strings(&panel, 0..20, cx),
6735            &[
6736                "v root",
6737                "    v dir1",
6738                "        v subdir1/nested1  <== selected",
6739                "              file1.txt",
6740                "              file2.txt",
6741                "        > subdir2",
6742                "    > dir2/single_file",
6743            ],
6744            "Initial state with some dirs expanded"
6745        );
6746
6747        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6748        panel.update(cx, |panel, cx| {
6749            let project = panel.project.read(cx);
6750            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6751            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6752        });
6753
6754        toggle_expand_dir(&panel, "root/dir1", cx);
6755
6756        assert_eq!(
6757            visible_entries_as_strings(&panel, 0..20, cx),
6758            &[
6759                "v root",
6760                "    v dir1  <== selected",
6761                "        > subdir1/nested1",
6762                "        > subdir2",
6763                "    > dir2/single_file",
6764            ],
6765            "Subdirs should be collapsed and folded with auto-fold enabled"
6766        );
6767    }
6768
6769    // Test 3: With auto-fold disabled
6770    {
6771        cx.update(|_, cx| {
6772            let settings = *ProjectPanelSettings::get_global(cx);
6773            ProjectPanelSettings::override_global(
6774                ProjectPanelSettings {
6775                    auto_fold_dirs: false,
6776                    ..settings
6777                },
6778                cx,
6779            );
6780        });
6781
6782        let panel = workspace.update_in(cx, ProjectPanel::new);
6783        cx.run_until_parked();
6784
6785        toggle_expand_dir(&panel, "root/dir1", cx);
6786        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6787        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6788
6789        assert_eq!(
6790            visible_entries_as_strings(&panel, 0..20, cx),
6791            &[
6792                "v root",
6793                "    v dir1",
6794                "        v subdir1",
6795                "            v nested1  <== selected",
6796                "                  file1.txt",
6797                "                  file2.txt",
6798                "        > subdir2",
6799                "    > dir2",
6800            ],
6801            "Initial state with some dirs expanded and auto-fold disabled"
6802        );
6803
6804        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6805        panel.update(cx, |panel, cx| {
6806            let project = panel.project.read(cx);
6807            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6808            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6809        });
6810
6811        toggle_expand_dir(&panel, "root/dir1", cx);
6812
6813        assert_eq!(
6814            visible_entries_as_strings(&panel, 0..20, cx),
6815            &[
6816                "v root",
6817                "    v dir1  <== selected",
6818                "        > subdir1",
6819                "        > subdir2",
6820                "    > dir2",
6821            ],
6822            "Subdirs should be collapsed but not folded with auto-fold disabled"
6823        );
6824    }
6825}
6826
6827#[gpui::test]
6828async fn test_collapse_selected_entry_and_children_action(cx: &mut gpui::TestAppContext) {
6829    init_test(cx);
6830
6831    let fs = FakeFs::new(cx.executor());
6832    fs.insert_tree(
6833        path!("/root"),
6834        json!({
6835            "dir1": {
6836                "subdir1": {
6837                    "nested1": {
6838                        "file1.txt": "",
6839                        "file2.txt": ""
6840                    },
6841                },
6842                "subdir2": {
6843                    "file3.txt": ""
6844                }
6845            },
6846            "dir2": {
6847                "file4.txt": ""
6848            }
6849        }),
6850    )
6851    .await;
6852
6853    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6854    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6855    let workspace = window
6856        .read_with(cx, |mw, _| mw.workspace().clone())
6857        .unwrap();
6858    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6859
6860    let panel = workspace.update_in(cx, ProjectPanel::new);
6861    cx.run_until_parked();
6862
6863    toggle_expand_dir(&panel, "root/dir1", cx);
6864    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6865    toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6866    toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
6867    toggle_expand_dir(&panel, "root/dir2", cx);
6868
6869    assert_eq!(
6870        visible_entries_as_strings(&panel, 0..20, cx),
6871        &[
6872            "v root",
6873            "    v dir1",
6874            "        v subdir1",
6875            "            v nested1",
6876            "                  file1.txt",
6877            "                  file2.txt",
6878            "        v subdir2",
6879            "              file3.txt",
6880            "    v dir2  <== selected",
6881            "          file4.txt",
6882        ],
6883        "Initial state with directories expanded"
6884    );
6885
6886    select_path(&panel, "root/dir1", cx);
6887    cx.run_until_parked();
6888
6889    panel.update_in(cx, |panel, window, cx| {
6890        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
6891    });
6892    cx.run_until_parked();
6893
6894    assert_eq!(
6895        visible_entries_as_strings(&panel, 0..20, cx),
6896        &[
6897            "v root",
6898            "    > dir1  <== selected",
6899            "    v dir2",
6900            "          file4.txt",
6901        ],
6902        "dir1 and all its children should be collapsed, dir2 should remain expanded"
6903    );
6904
6905    toggle_expand_dir(&panel, "root/dir1", cx);
6906    cx.run_until_parked();
6907
6908    assert_eq!(
6909        visible_entries_as_strings(&panel, 0..20, cx),
6910        &[
6911            "v root",
6912            "    v dir1  <== selected",
6913            "        > subdir1",
6914            "        > subdir2",
6915            "    v dir2",
6916            "          file4.txt",
6917        ],
6918        "After re-expanding dir1, its children should still be collapsed"
6919    );
6920}
6921
6922#[gpui::test]
6923async fn test_collapse_root_single_worktree(cx: &mut gpui::TestAppContext) {
6924    init_test(cx);
6925
6926    let fs = FakeFs::new(cx.executor());
6927    fs.insert_tree(
6928        path!("/root"),
6929        json!({
6930            "dir1": {
6931                "subdir1": {
6932                    "file1.txt": ""
6933                },
6934                "file2.txt": ""
6935            },
6936            "dir2": {
6937                "file3.txt": ""
6938            }
6939        }),
6940    )
6941    .await;
6942
6943    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6944    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6945    let workspace = window
6946        .read_with(cx, |mw, _| mw.workspace().clone())
6947        .unwrap();
6948    let cx = &mut VisualTestContext::from_window(window.into(), cx);
6949
6950    let panel = workspace.update_in(cx, ProjectPanel::new);
6951    cx.run_until_parked();
6952
6953    toggle_expand_dir(&panel, "root/dir1", cx);
6954    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6955    toggle_expand_dir(&panel, "root/dir2", cx);
6956
6957    assert_eq!(
6958        visible_entries_as_strings(&panel, 0..20, cx),
6959        &[
6960            "v root",
6961            "    v dir1",
6962            "        v subdir1",
6963            "              file1.txt",
6964            "          file2.txt",
6965            "    v dir2  <== selected",
6966            "          file3.txt",
6967        ],
6968        "Initial state with directories expanded"
6969    );
6970
6971    // Select the root and collapse it and its children
6972    select_path(&panel, "root", cx);
6973    cx.run_until_parked();
6974
6975    panel.update_in(cx, |panel, window, cx| {
6976        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
6977    });
6978    cx.run_until_parked();
6979
6980    // The root and all its children should be collapsed
6981    assert_eq!(
6982        visible_entries_as_strings(&panel, 0..20, cx),
6983        &["> root  <== selected"],
6984        "Root and all children should be collapsed"
6985    );
6986
6987    // Re-expand root and dir1, verify children were recursively collapsed
6988    toggle_expand_dir(&panel, "root", cx);
6989    toggle_expand_dir(&panel, "root/dir1", cx);
6990    cx.run_until_parked();
6991
6992    assert_eq!(
6993        visible_entries_as_strings(&panel, 0..20, cx),
6994        &[
6995            "v root",
6996            "    v dir1  <== selected",
6997            "        > subdir1",
6998            "          file2.txt",
6999            "    > dir2",
7000        ],
7001        "After re-expanding root and dir1, subdir1 should still be collapsed"
7002    );
7003}
7004
7005#[gpui::test]
7006async fn test_collapse_root_multi_worktree(cx: &mut gpui::TestAppContext) {
7007    init_test(cx);
7008
7009    let fs = FakeFs::new(cx.executor());
7010    fs.insert_tree(
7011        path!("/root1"),
7012        json!({
7013            "dir1": {
7014                "subdir1": {
7015                    "file1.txt": ""
7016                },
7017                "file2.txt": ""
7018            }
7019        }),
7020    )
7021    .await;
7022    fs.insert_tree(
7023        path!("/root2"),
7024        json!({
7025            "dir2": {
7026                "file3.txt": ""
7027            },
7028            "file4.txt": ""
7029        }),
7030    )
7031    .await;
7032
7033    let project = Project::test(
7034        fs.clone(),
7035        [path!("/root1").as_ref(), path!("/root2").as_ref()],
7036        cx,
7037    )
7038    .await;
7039    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7040    let workspace = window
7041        .read_with(cx, |mw, _| mw.workspace().clone())
7042        .unwrap();
7043    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7044
7045    let panel = workspace.update_in(cx, ProjectPanel::new);
7046    cx.run_until_parked();
7047
7048    toggle_expand_dir(&panel, "root1/dir1", cx);
7049    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
7050    toggle_expand_dir(&panel, "root2/dir2", cx);
7051
7052    assert_eq!(
7053        visible_entries_as_strings(&panel, 0..20, cx),
7054        &[
7055            "v root1",
7056            "    v dir1",
7057            "        v subdir1",
7058            "              file1.txt",
7059            "          file2.txt",
7060            "v root2",
7061            "    v dir2  <== selected",
7062            "          file3.txt",
7063            "      file4.txt",
7064        ],
7065        "Initial state with directories expanded across worktrees"
7066    );
7067
7068    // Select root1 and collapse it and its children.
7069    // In a multi-worktree project, this should only collapse the selected worktree,
7070    // leaving other worktrees unaffected.
7071    select_path(&panel, "root1", cx);
7072    cx.run_until_parked();
7073
7074    panel.update_in(cx, |panel, window, cx| {
7075        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
7076    });
7077    cx.run_until_parked();
7078
7079    assert_eq!(
7080        visible_entries_as_strings(&panel, 0..20, cx),
7081        &[
7082            "> root1  <== selected",
7083            "v root2",
7084            "    v dir2",
7085            "          file3.txt",
7086            "      file4.txt",
7087        ],
7088        "Only root1 should be collapsed, root2 should remain expanded"
7089    );
7090
7091    // Re-expand root1 and verify its children were recursively collapsed
7092    toggle_expand_dir(&panel, "root1", cx);
7093
7094    assert_eq!(
7095        visible_entries_as_strings(&panel, 0..20, cx),
7096        &[
7097            "v root1  <== selected",
7098            "    > dir1",
7099            "v root2",
7100            "    v dir2",
7101            "          file3.txt",
7102            "      file4.txt",
7103        ],
7104        "After re-expanding root1, dir1 should still be collapsed, root2 should be unaffected"
7105    );
7106}
7107
7108#[gpui::test]
7109async fn test_collapse_non_root_multi_worktree(cx: &mut gpui::TestAppContext) {
7110    init_test(cx);
7111
7112    let fs = FakeFs::new(cx.executor());
7113    fs.insert_tree(
7114        path!("/root1"),
7115        json!({
7116            "dir1": {
7117                "subdir1": {
7118                    "file1.txt": ""
7119                },
7120                "file2.txt": ""
7121            }
7122        }),
7123    )
7124    .await;
7125    fs.insert_tree(
7126        path!("/root2"),
7127        json!({
7128            "dir2": {
7129                "subdir2": {
7130                    "file3.txt": ""
7131                },
7132                "file4.txt": ""
7133            }
7134        }),
7135    )
7136    .await;
7137
7138    let project = Project::test(
7139        fs.clone(),
7140        [path!("/root1").as_ref(), path!("/root2").as_ref()],
7141        cx,
7142    )
7143    .await;
7144    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7145    let workspace = window
7146        .read_with(cx, |mw, _| mw.workspace().clone())
7147        .unwrap();
7148    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7149
7150    let panel = workspace.update_in(cx, ProjectPanel::new);
7151    cx.run_until_parked();
7152
7153    toggle_expand_dir(&panel, "root1/dir1", cx);
7154    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
7155    toggle_expand_dir(&panel, "root2/dir2", cx);
7156    toggle_expand_dir(&panel, "root2/dir2/subdir2", cx);
7157
7158    assert_eq!(
7159        visible_entries_as_strings(&panel, 0..20, cx),
7160        &[
7161            "v root1",
7162            "    v dir1",
7163            "        v subdir1",
7164            "              file1.txt",
7165            "          file2.txt",
7166            "v root2",
7167            "    v dir2",
7168            "        v subdir2  <== selected",
7169            "              file3.txt",
7170            "          file4.txt",
7171        ],
7172        "Initial state with directories expanded across worktrees"
7173    );
7174
7175    // Select dir1 in root1 and collapse it
7176    select_path(&panel, "root1/dir1", cx);
7177    cx.run_until_parked();
7178
7179    panel.update_in(cx, |panel, window, cx| {
7180        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
7181    });
7182    cx.run_until_parked();
7183
7184    assert_eq!(
7185        visible_entries_as_strings(&panel, 0..20, cx),
7186        &[
7187            "v root1",
7188            "    > dir1  <== selected",
7189            "v root2",
7190            "    v dir2",
7191            "        v subdir2",
7192            "              file3.txt",
7193            "          file4.txt",
7194        ],
7195        "Only dir1 should be collapsed, root2 should be completely unaffected"
7196    );
7197
7198    // Re-expand dir1 and verify subdir1 was recursively collapsed
7199    toggle_expand_dir(&panel, "root1/dir1", cx);
7200
7201    assert_eq!(
7202        visible_entries_as_strings(&panel, 0..20, cx),
7203        &[
7204            "v root1",
7205            "    v dir1  <== selected",
7206            "        > subdir1",
7207            "          file2.txt",
7208            "v root2",
7209            "    v dir2",
7210            "        v subdir2",
7211            "              file3.txt",
7212            "          file4.txt",
7213        ],
7214        "After re-expanding dir1, subdir1 should still be collapsed"
7215    );
7216}
7217
7218#[gpui::test]
7219async fn test_collapse_all_for_root_single_worktree(cx: &mut gpui::TestAppContext) {
7220    init_test(cx);
7221
7222    let fs = FakeFs::new(cx.executor());
7223    fs.insert_tree(
7224        path!("/root"),
7225        json!({
7226            "dir1": {
7227                "subdir1": {
7228                    "file1.txt": ""
7229                },
7230                "file2.txt": ""
7231            },
7232            "dir2": {
7233                "file3.txt": ""
7234            }
7235        }),
7236    )
7237    .await;
7238
7239    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7240    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7241    let workspace = window
7242        .read_with(cx, |mw, _| mw.workspace().clone())
7243        .unwrap();
7244    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7245
7246    let panel = workspace.update_in(cx, ProjectPanel::new);
7247    cx.run_until_parked();
7248
7249    toggle_expand_dir(&panel, "root/dir1", cx);
7250    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
7251    toggle_expand_dir(&panel, "root/dir2", cx);
7252
7253    assert_eq!(
7254        visible_entries_as_strings(&panel, 0..20, cx),
7255        &[
7256            "v root",
7257            "    v dir1",
7258            "        v subdir1",
7259            "              file1.txt",
7260            "          file2.txt",
7261            "    v dir2  <== selected",
7262            "          file3.txt",
7263        ],
7264        "Initial state with directories expanded"
7265    );
7266
7267    select_path(&panel, "root", cx);
7268    cx.run_until_parked();
7269
7270    panel.update_in(cx, |panel, window, cx| {
7271        panel.collapse_all_for_root(window, cx);
7272    });
7273    cx.run_until_parked();
7274
7275    assert_eq!(
7276        visible_entries_as_strings(&panel, 0..20, cx),
7277        &["v root  <== selected", "    > dir1", "    > dir2"],
7278        "Root should remain expanded but all children should be collapsed"
7279    );
7280
7281    toggle_expand_dir(&panel, "root/dir1", cx);
7282    cx.run_until_parked();
7283
7284    assert_eq!(
7285        visible_entries_as_strings(&panel, 0..20, cx),
7286        &[
7287            "v root",
7288            "    v dir1  <== selected",
7289            "        > subdir1",
7290            "          file2.txt",
7291            "    > dir2",
7292        ],
7293        "After re-expanding dir1, subdir1 should still be collapsed"
7294    );
7295}
7296
7297#[gpui::test]
7298async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext) {
7299    init_test(cx);
7300
7301    let fs = FakeFs::new(cx.executor());
7302    fs.insert_tree(
7303        path!("/root1"),
7304        json!({
7305            "dir1": {
7306                "subdir1": {
7307                    "file1.txt": ""
7308                },
7309                "file2.txt": ""
7310            }
7311        }),
7312    )
7313    .await;
7314    fs.insert_tree(
7315        path!("/root2"),
7316        json!({
7317            "dir2": {
7318                "file3.txt": ""
7319            },
7320            "file4.txt": ""
7321        }),
7322    )
7323    .await;
7324
7325    let project = Project::test(
7326        fs.clone(),
7327        [path!("/root1").as_ref(), path!("/root2").as_ref()],
7328        cx,
7329    )
7330    .await;
7331    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7332    let workspace = window
7333        .read_with(cx, |mw, _| mw.workspace().clone())
7334        .unwrap();
7335    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7336
7337    let panel = workspace.update_in(cx, ProjectPanel::new);
7338    cx.run_until_parked();
7339
7340    toggle_expand_dir(&panel, "root1/dir1", cx);
7341    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
7342    toggle_expand_dir(&panel, "root2/dir2", cx);
7343
7344    assert_eq!(
7345        visible_entries_as_strings(&panel, 0..20, cx),
7346        &[
7347            "v root1",
7348            "    v dir1",
7349            "        v subdir1",
7350            "              file1.txt",
7351            "          file2.txt",
7352            "v root2",
7353            "    v dir2  <== selected",
7354            "          file3.txt",
7355            "      file4.txt",
7356        ],
7357        "Initial state with directories expanded across worktrees"
7358    );
7359
7360    select_path(&panel, "root1", cx);
7361    cx.run_until_parked();
7362
7363    panel.update_in(cx, |panel, window, cx| {
7364        panel.collapse_all_for_root(window, cx);
7365    });
7366    cx.run_until_parked();
7367
7368    assert_eq!(
7369        visible_entries_as_strings(&panel, 0..20, cx),
7370        &[
7371            "> root1  <== selected",
7372            "v root2",
7373            "    v dir2",
7374            "          file3.txt",
7375            "      file4.txt",
7376        ],
7377        "With multiple worktrees, root1 should collapse completely (including itself)"
7378    );
7379}
7380
7381#[gpui::test]
7382async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppContext) {
7383    init_test(cx);
7384
7385    let fs = FakeFs::new(cx.executor());
7386    fs.insert_tree(
7387        path!("/root"),
7388        json!({
7389            "dir1": {
7390                "subdir1": {
7391                    "file1.txt": ""
7392                },
7393            },
7394            "dir2": {
7395                "file2.txt": ""
7396            }
7397        }),
7398    )
7399    .await;
7400
7401    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7402    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7403    let workspace = window
7404        .read_with(cx, |mw, _| mw.workspace().clone())
7405        .unwrap();
7406    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7407
7408    let panel = workspace.update_in(cx, ProjectPanel::new);
7409    cx.run_until_parked();
7410
7411    toggle_expand_dir(&panel, "root/dir1", cx);
7412    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
7413    toggle_expand_dir(&panel, "root/dir2", cx);
7414
7415    assert_eq!(
7416        visible_entries_as_strings(&panel, 0..20, cx),
7417        &[
7418            "v root",
7419            "    v dir1",
7420            "        v subdir1",
7421            "              file1.txt",
7422            "    v dir2  <== selected",
7423            "          file2.txt",
7424        ],
7425        "Initial state with directories expanded"
7426    );
7427
7428    select_path(&panel, "root/dir1", cx);
7429    cx.run_until_parked();
7430
7431    panel.update_in(cx, |panel, window, cx| {
7432        panel.collapse_all_for_root(window, cx);
7433    });
7434    cx.run_until_parked();
7435
7436    assert_eq!(
7437        visible_entries_as_strings(&panel, 0..20, cx),
7438        &[
7439            "v root",
7440            "    v dir1  <== selected",
7441            "        v subdir1",
7442            "              file1.txt",
7443            "    v dir2",
7444            "          file2.txt",
7445        ],
7446        "collapse_all_for_root should be a no-op when called on a non-root directory"
7447    );
7448}
7449
7450#[gpui::test]
7451async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
7452    init_test(cx);
7453
7454    let fs = FakeFs::new(cx.executor());
7455    fs.insert_tree(
7456        path!("/root"),
7457        json!({
7458            "dir1": {
7459                "file1.txt": "",
7460            },
7461        }),
7462    )
7463    .await;
7464
7465    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7466    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7467    let workspace = window
7468        .read_with(cx, |mw, _| mw.workspace().clone())
7469        .unwrap();
7470    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7471
7472    let panel = workspace.update_in(cx, |workspace, window, cx| {
7473        let panel = ProjectPanel::new(workspace, window, cx);
7474        workspace.add_panel(panel.clone(), window, cx);
7475        panel
7476    });
7477    cx.run_until_parked();
7478
7479    #[rustfmt::skip]
7480    assert_eq!(
7481        visible_entries_as_strings(&panel, 0..20, cx),
7482        &[
7483            "v root",
7484            "    > dir1",
7485        ],
7486        "Initial state with nothing selected"
7487    );
7488
7489    panel.update_in(cx, |panel, window, cx| {
7490        panel.new_file(&NewFile, window, cx);
7491    });
7492    cx.run_until_parked();
7493    panel.update_in(cx, |panel, window, cx| {
7494        assert!(panel.filename_editor.read(cx).is_focused(window));
7495    });
7496    panel
7497        .update_in(cx, |panel, window, cx| {
7498            panel.filename_editor.update(cx, |editor, cx| {
7499                editor.set_text("hello_from_no_selections", window, cx)
7500            });
7501            panel.confirm_edit(true, window, cx).unwrap()
7502        })
7503        .await
7504        .unwrap();
7505    cx.run_until_parked();
7506    #[rustfmt::skip]
7507    assert_eq!(
7508        visible_entries_as_strings(&panel, 0..20, cx),
7509        &[
7510            "v root",
7511            "    > dir1",
7512            "      hello_from_no_selections  <== selected  <== marked",
7513        ],
7514        "A new file is created under the root directory"
7515    );
7516}
7517
7518#[gpui::test]
7519async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
7520    init_test(cx);
7521
7522    let fs = FakeFs::new(cx.executor());
7523    fs.insert_tree(
7524        path!("/root"),
7525        json!({
7526            "existing_dir": {
7527                "existing_file.txt": "",
7528            },
7529            "existing_file.txt": "",
7530        }),
7531    )
7532    .await;
7533
7534    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7535    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7536    let workspace = window
7537        .read_with(cx, |mw, _| mw.workspace().clone())
7538        .unwrap();
7539    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7540
7541    cx.update(|_, cx| {
7542        let settings = *ProjectPanelSettings::get_global(cx);
7543        ProjectPanelSettings::override_global(
7544            ProjectPanelSettings {
7545                hide_root: true,
7546                ..settings
7547            },
7548            cx,
7549        );
7550    });
7551
7552    let panel = workspace.update_in(cx, |workspace, window, cx| {
7553        let panel = ProjectPanel::new(workspace, window, cx);
7554        workspace.add_panel(panel.clone(), window, cx);
7555        panel
7556    });
7557    cx.run_until_parked();
7558
7559    #[rustfmt::skip]
7560    assert_eq!(
7561        visible_entries_as_strings(&panel, 0..20, cx),
7562        &[
7563            "> existing_dir",
7564            "  existing_file.txt",
7565        ],
7566        "Initial state with hide_root=true, root should be hidden and nothing selected"
7567    );
7568
7569    panel.update(cx, |panel, _| {
7570        assert!(
7571            panel.selection.is_none(),
7572            "Should have no selection initially"
7573        );
7574    });
7575
7576    // Test 1: Create new file when no entry is selected
7577    panel.update_in(cx, |panel, window, cx| {
7578        panel.new_file(&NewFile, window, cx);
7579    });
7580    cx.run_until_parked();
7581    panel.update_in(cx, |panel, window, cx| {
7582        assert!(panel.filename_editor.read(cx).is_focused(window));
7583    });
7584    cx.run_until_parked();
7585    #[rustfmt::skip]
7586    assert_eq!(
7587        visible_entries_as_strings(&panel, 0..20, cx),
7588        &[
7589            "> existing_dir",
7590            "  [EDITOR: '']  <== selected",
7591            "  existing_file.txt",
7592        ],
7593        "Editor should appear at root level when hide_root=true and no selection"
7594    );
7595
7596    let confirm = panel.update_in(cx, |panel, window, cx| {
7597        panel.filename_editor.update(cx, |editor, cx| {
7598            editor.set_text("new_file_at_root.txt", window, cx)
7599        });
7600        panel.confirm_edit(true, window, cx).unwrap()
7601    });
7602    confirm.await.unwrap();
7603    cx.run_until_parked();
7604
7605    #[rustfmt::skip]
7606    assert_eq!(
7607        visible_entries_as_strings(&panel, 0..20, cx),
7608        &[
7609            "> existing_dir",
7610            "  existing_file.txt",
7611            "  new_file_at_root.txt  <== selected  <== marked",
7612        ],
7613        "New file should be created at root level and visible without root prefix"
7614    );
7615
7616    assert!(
7617        fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
7618        "File should be created in the actual root directory"
7619    );
7620
7621    // Test 2: Create new directory when no entry is selected
7622    panel.update(cx, |panel, _| {
7623        panel.selection = None;
7624    });
7625
7626    panel.update_in(cx, |panel, window, cx| {
7627        panel.new_directory(&NewDirectory, window, cx);
7628    });
7629    cx.run_until_parked();
7630
7631    panel.update_in(cx, |panel, window, cx| {
7632        assert!(panel.filename_editor.read(cx).is_focused(window));
7633    });
7634
7635    #[rustfmt::skip]
7636    assert_eq!(
7637        visible_entries_as_strings(&panel, 0..20, cx),
7638        &[
7639            "> [EDITOR: '']  <== selected",
7640            "> existing_dir",
7641            "  existing_file.txt",
7642            "  new_file_at_root.txt",
7643        ],
7644        "Directory editor should appear at root level when hide_root=true and no selection"
7645    );
7646
7647    let confirm = panel.update_in(cx, |panel, window, cx| {
7648        panel.filename_editor.update(cx, |editor, cx| {
7649            editor.set_text("new_dir_at_root", window, cx)
7650        });
7651        panel.confirm_edit(true, window, cx).unwrap()
7652    });
7653    confirm.await.unwrap();
7654    cx.run_until_parked();
7655
7656    #[rustfmt::skip]
7657    assert_eq!(
7658        visible_entries_as_strings(&panel, 0..20, cx),
7659        &[
7660            "> existing_dir",
7661            "v new_dir_at_root  <== selected",
7662            "  existing_file.txt",
7663            "  new_file_at_root.txt",
7664        ],
7665        "New directory should be created at root level and visible without root prefix"
7666    );
7667
7668    assert!(
7669        fs.is_dir(Path::new("/root/new_dir_at_root")).await,
7670        "Directory should be created in the actual root directory"
7671    );
7672}
7673
7674#[cfg(windows)]
7675#[gpui::test]
7676async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) {
7677    init_test(cx);
7678
7679    let fs = FakeFs::new(cx.executor());
7680    fs.insert_tree(
7681        path!("/root"),
7682        json!({
7683            "dir1": {
7684                "file1.txt": "",
7685            },
7686        }),
7687    )
7688    .await;
7689
7690    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7691    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7692    let workspace = window
7693        .read_with(cx, |mw, _| mw.workspace().clone())
7694        .unwrap();
7695    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7696
7697    let panel = workspace.update_in(cx, |workspace, window, cx| {
7698        let panel = ProjectPanel::new(workspace, window, cx);
7699        workspace.add_panel(panel.clone(), window, cx);
7700        panel
7701    });
7702    cx.run_until_parked();
7703
7704    #[rustfmt::skip]
7705    assert_eq!(
7706        visible_entries_as_strings(&panel, 0..20, cx),
7707        &[
7708            "v root",
7709            "    > dir1",
7710        ],
7711        "Initial state with nothing selected"
7712    );
7713
7714    panel.update_in(cx, |panel, window, cx| {
7715        panel.new_file(&NewFile, window, cx);
7716    });
7717    cx.run_until_parked();
7718    panel.update_in(cx, |panel, window, cx| {
7719        assert!(panel.filename_editor.read(cx).is_focused(window));
7720    });
7721    panel
7722        .update_in(cx, |panel, window, cx| {
7723            panel
7724                .filename_editor
7725                .update(cx, |editor, cx| editor.set_text("foo.", window, cx));
7726            panel.confirm_edit(true, window, cx).unwrap()
7727        })
7728        .await
7729        .unwrap();
7730    cx.run_until_parked();
7731    #[rustfmt::skip]
7732    assert_eq!(
7733        visible_entries_as_strings(&panel, 0..20, cx),
7734        &[
7735            "v root",
7736            "    > dir1",
7737            "      foo  <== selected  <== marked",
7738        ],
7739        "A new file is created under the root directory without the trailing dot"
7740    );
7741}
7742
7743#[gpui::test]
7744async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
7745    init_test(cx);
7746
7747    let fs = FakeFs::new(cx.executor());
7748    fs.insert_tree(
7749        "/root",
7750        json!({
7751            "dir1": {
7752                "file1.txt": "",
7753                "dir2": {
7754                    "file2.txt": ""
7755                }
7756            },
7757            "file3.txt": ""
7758        }),
7759    )
7760    .await;
7761
7762    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7763    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7764    let workspace = window
7765        .read_with(cx, |mw, _| mw.workspace().clone())
7766        .unwrap();
7767    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7768    let panel = workspace.update_in(cx, ProjectPanel::new);
7769    cx.run_until_parked();
7770
7771    panel.update(cx, |panel, cx| {
7772        let project = panel.project.read(cx);
7773        let worktree = project.visible_worktrees(cx).next().unwrap();
7774        let worktree = worktree.read(cx);
7775
7776        // Test 1: Target is a directory, should highlight the directory itself
7777        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
7778        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
7779        assert_eq!(
7780            result,
7781            Some(dir_entry.id),
7782            "Should highlight directory itself"
7783        );
7784
7785        // Test 2: Target is nested file, should highlight immediate parent
7786        let nested_file = worktree
7787            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
7788            .unwrap();
7789        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
7790        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
7791        assert_eq!(
7792            result,
7793            Some(nested_parent.id),
7794            "Should highlight immediate parent"
7795        );
7796
7797        // Test 3: Target is root level file, should highlight root
7798        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
7799        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
7800        assert_eq!(
7801            result,
7802            Some(worktree.root_entry().unwrap().id),
7803            "Root level file should return None"
7804        );
7805
7806        // Test 4: Target is root itself, should highlight root
7807        let root_entry = worktree.root_entry().unwrap();
7808        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
7809        assert_eq!(
7810            result,
7811            Some(root_entry.id),
7812            "Root level file should return None"
7813        );
7814    });
7815}
7816
7817#[gpui::test]
7818async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
7819    init_test(cx);
7820
7821    let fs = FakeFs::new(cx.executor());
7822    fs.insert_tree(
7823        "/root",
7824        json!({
7825            "parent_dir": {
7826                "child_file.txt": "",
7827                "sibling_file.txt": "",
7828                "child_dir": {
7829                    "nested_file.txt": ""
7830                }
7831            },
7832            "other_dir": {
7833                "other_file.txt": ""
7834            }
7835        }),
7836    )
7837    .await;
7838
7839    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7840    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7841    let workspace = window
7842        .read_with(cx, |mw, _| mw.workspace().clone())
7843        .unwrap();
7844    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7845    let panel = workspace.update_in(cx, ProjectPanel::new);
7846    cx.run_until_parked();
7847
7848    panel.update(cx, |panel, cx| {
7849        let project = panel.project.read(cx);
7850        let worktree = project.visible_worktrees(cx).next().unwrap();
7851        let worktree_id = worktree.read(cx).id();
7852        let worktree = worktree.read(cx);
7853
7854        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
7855        let child_file = worktree
7856            .entry_for_path(rel_path("parent_dir/child_file.txt"))
7857            .unwrap();
7858        let sibling_file = worktree
7859            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
7860            .unwrap();
7861        let child_dir = worktree
7862            .entry_for_path(rel_path("parent_dir/child_dir"))
7863            .unwrap();
7864        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
7865        let other_file = worktree
7866            .entry_for_path(rel_path("other_dir/other_file.txt"))
7867            .unwrap();
7868
7869        // Test 1: Single item drag, don't highlight parent directory
7870        let dragged_selection = DraggedSelection {
7871            active_selection: SelectedEntry {
7872                worktree_id,
7873                entry_id: child_file.id,
7874            },
7875            marked_selections: Arc::new([SelectedEntry {
7876                worktree_id,
7877                entry_id: child_file.id,
7878            }]),
7879        };
7880        let result =
7881            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
7882        assert_eq!(result, None, "Should not highlight parent of dragged item");
7883
7884        // Test 2: Single item drag, don't highlight sibling files
7885        let result = panel.highlight_entry_for_selection_drag(
7886            sibling_file,
7887            worktree,
7888            &dragged_selection,
7889            cx,
7890        );
7891        assert_eq!(result, None, "Should not highlight sibling files");
7892
7893        // Test 3: Single item drag, highlight unrelated directory
7894        let result =
7895            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
7896        assert_eq!(
7897            result,
7898            Some(other_dir.id),
7899            "Should highlight unrelated directory"
7900        );
7901
7902        // Test 4: Single item drag, highlight sibling directory
7903        let result =
7904            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
7905        assert_eq!(
7906            result,
7907            Some(child_dir.id),
7908            "Should highlight sibling directory"
7909        );
7910
7911        // Test 5: Multiple items drag, highlight parent directory
7912        let dragged_selection = DraggedSelection {
7913            active_selection: SelectedEntry {
7914                worktree_id,
7915                entry_id: child_file.id,
7916            },
7917            marked_selections: Arc::new([
7918                SelectedEntry {
7919                    worktree_id,
7920                    entry_id: child_file.id,
7921                },
7922                SelectedEntry {
7923                    worktree_id,
7924                    entry_id: sibling_file.id,
7925                },
7926            ]),
7927        };
7928        let result =
7929            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
7930        assert_eq!(
7931            result,
7932            Some(parent_dir.id),
7933            "Should highlight parent with multiple items"
7934        );
7935
7936        // Test 6: Target is file in different directory, highlight parent
7937        let result =
7938            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
7939        assert_eq!(
7940            result,
7941            Some(other_dir.id),
7942            "Should highlight parent of target file"
7943        );
7944
7945        // Test 7: Target is directory, always highlight
7946        let result =
7947            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
7948        assert_eq!(
7949            result,
7950            Some(child_dir.id),
7951            "Should always highlight directories"
7952        );
7953    });
7954}
7955
7956#[gpui::test]
7957async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
7958    init_test(cx);
7959
7960    let fs = FakeFs::new(cx.executor());
7961    fs.insert_tree(
7962        "/root1",
7963        json!({
7964            "src": {
7965                "main.rs": "",
7966                "lib.rs": ""
7967            }
7968        }),
7969    )
7970    .await;
7971    fs.insert_tree(
7972        "/root2",
7973        json!({
7974            "src": {
7975                "main.rs": "",
7976                "test.rs": ""
7977            }
7978        }),
7979    )
7980    .await;
7981
7982    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7983    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7984    let workspace = window
7985        .read_with(cx, |mw, _| mw.workspace().clone())
7986        .unwrap();
7987    let cx = &mut VisualTestContext::from_window(window.into(), cx);
7988    let panel = workspace.update_in(cx, ProjectPanel::new);
7989    cx.run_until_parked();
7990
7991    panel.update(cx, |panel, cx| {
7992        let project = panel.project.read(cx);
7993        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
7994
7995        let worktree_a = &worktrees[0];
7996        let main_rs_from_a = worktree_a
7997            .read(cx)
7998            .entry_for_path(rel_path("src/main.rs"))
7999            .unwrap();
8000
8001        let worktree_b = &worktrees[1];
8002        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
8003        let main_rs_from_b = worktree_b
8004            .read(cx)
8005            .entry_for_path(rel_path("src/main.rs"))
8006            .unwrap();
8007
8008        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
8009        let dragged_selection = DraggedSelection {
8010            active_selection: SelectedEntry {
8011                worktree_id: worktree_a.read(cx).id(),
8012                entry_id: main_rs_from_a.id,
8013            },
8014            marked_selections: Arc::new([SelectedEntry {
8015                worktree_id: worktree_a.read(cx).id(),
8016                entry_id: main_rs_from_a.id,
8017            }]),
8018        };
8019
8020        let result = panel.highlight_entry_for_selection_drag(
8021            src_dir_from_b,
8022            worktree_b.read(cx),
8023            &dragged_selection,
8024            cx,
8025        );
8026        assert_eq!(
8027            result,
8028            Some(src_dir_from_b.id),
8029            "Should highlight target directory from different worktree even with same relative path"
8030        );
8031
8032        // Test dragging file from worktree A onto file with same relative path in worktree B
8033        let result = panel.highlight_entry_for_selection_drag(
8034            main_rs_from_b,
8035            worktree_b.read(cx),
8036            &dragged_selection,
8037            cx,
8038        );
8039        assert_eq!(
8040            result,
8041            Some(src_dir_from_b.id),
8042            "Should highlight parent of target file from different worktree"
8043        );
8044    });
8045}
8046
8047#[gpui::test]
8048async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
8049    init_test(cx);
8050
8051    let fs = FakeFs::new(cx.executor());
8052    fs.insert_tree(
8053        "/root1",
8054        json!({
8055            "parent_dir": {
8056                "child_file.txt": "",
8057                "nested_dir": {
8058                    "nested_file.txt": ""
8059                }
8060            },
8061            "root_file.txt": ""
8062        }),
8063    )
8064    .await;
8065
8066    fs.insert_tree(
8067        "/root2",
8068        json!({
8069            "other_dir": {
8070                "other_file.txt": ""
8071            }
8072        }),
8073    )
8074    .await;
8075
8076    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
8077    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8078    let workspace = window
8079        .read_with(cx, |mw, _| mw.workspace().clone())
8080        .unwrap();
8081    let cx = &mut VisualTestContext::from_window(window.into(), cx);
8082    let panel = workspace.update_in(cx, ProjectPanel::new);
8083    cx.run_until_parked();
8084
8085    panel.update(cx, |panel, cx| {
8086        let project = panel.project.read(cx);
8087        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
8088        let worktree1 = worktrees[0].read(cx);
8089        let worktree2 = worktrees[1].read(cx);
8090        let worktree1_id = worktree1.id();
8091        let _worktree2_id = worktree2.id();
8092
8093        let root1_entry = worktree1.root_entry().unwrap();
8094        let root2_entry = worktree2.root_entry().unwrap();
8095        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
8096        let child_file = worktree1
8097            .entry_for_path(rel_path("parent_dir/child_file.txt"))
8098            .unwrap();
8099        let nested_file = worktree1
8100            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
8101            .unwrap();
8102        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
8103
8104        // Test 1: Multiple entries - should always highlight background
8105        let multiple_dragged_selection = DraggedSelection {
8106            active_selection: SelectedEntry {
8107                worktree_id: worktree1_id,
8108                entry_id: child_file.id,
8109            },
8110            marked_selections: Arc::new([
8111                SelectedEntry {
8112                    worktree_id: worktree1_id,
8113                    entry_id: child_file.id,
8114                },
8115                SelectedEntry {
8116                    worktree_id: worktree1_id,
8117                    entry_id: nested_file.id,
8118                },
8119            ]),
8120        };
8121
8122        let result = panel.should_highlight_background_for_selection_drag(
8123            &multiple_dragged_selection,
8124            root1_entry.id,
8125            cx,
8126        );
8127        assert!(result, "Should highlight background for multiple entries");
8128
8129        // Test 2: Single entry with non-empty parent path - should highlight background
8130        let nested_dragged_selection = DraggedSelection {
8131            active_selection: SelectedEntry {
8132                worktree_id: worktree1_id,
8133                entry_id: nested_file.id,
8134            },
8135            marked_selections: Arc::new([SelectedEntry {
8136                worktree_id: worktree1_id,
8137                entry_id: nested_file.id,
8138            }]),
8139        };
8140
8141        let result = panel.should_highlight_background_for_selection_drag(
8142            &nested_dragged_selection,
8143            root1_entry.id,
8144            cx,
8145        );
8146        assert!(result, "Should highlight background for nested file");
8147
8148        // Test 3: Single entry at root level, same worktree - should NOT highlight background
8149        let root_file_dragged_selection = DraggedSelection {
8150            active_selection: SelectedEntry {
8151                worktree_id: worktree1_id,
8152                entry_id: root_file.id,
8153            },
8154            marked_selections: Arc::new([SelectedEntry {
8155                worktree_id: worktree1_id,
8156                entry_id: root_file.id,
8157            }]),
8158        };
8159
8160        let result = panel.should_highlight_background_for_selection_drag(
8161            &root_file_dragged_selection,
8162            root1_entry.id,
8163            cx,
8164        );
8165        assert!(
8166            !result,
8167            "Should NOT highlight background for root file in same worktree"
8168        );
8169
8170        // Test 4: Single entry at root level, different worktree - should highlight background
8171        let result = panel.should_highlight_background_for_selection_drag(
8172            &root_file_dragged_selection,
8173            root2_entry.id,
8174            cx,
8175        );
8176        assert!(
8177            result,
8178            "Should highlight background for root file from different worktree"
8179        );
8180
8181        // Test 5: Single entry in subdirectory - should highlight background
8182        let child_file_dragged_selection = DraggedSelection {
8183            active_selection: SelectedEntry {
8184                worktree_id: worktree1_id,
8185                entry_id: child_file.id,
8186            },
8187            marked_selections: Arc::new([SelectedEntry {
8188                worktree_id: worktree1_id,
8189                entry_id: child_file.id,
8190            }]),
8191        };
8192
8193        let result = panel.should_highlight_background_for_selection_drag(
8194            &child_file_dragged_selection,
8195            root1_entry.id,
8196            cx,
8197        );
8198        assert!(
8199            result,
8200            "Should highlight background for file with non-empty parent path"
8201        );
8202    });
8203}
8204
8205#[gpui::test]
8206async fn test_hide_root(cx: &mut gpui::TestAppContext) {
8207    init_test(cx);
8208
8209    let fs = FakeFs::new(cx.executor());
8210    fs.insert_tree(
8211        "/root1",
8212        json!({
8213            "dir1": {
8214                "file1.txt": "content",
8215                "file2.txt": "content",
8216            },
8217            "dir2": {
8218                "file3.txt": "content",
8219            },
8220            "file4.txt": "content",
8221        }),
8222    )
8223    .await;
8224
8225    fs.insert_tree(
8226        "/root2",
8227        json!({
8228            "dir3": {
8229                "file5.txt": "content",
8230            },
8231            "file6.txt": "content",
8232        }),
8233    )
8234    .await;
8235
8236    // Test 1: Single worktree with hide_root = false
8237    {
8238        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
8239        let window =
8240            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8241        let workspace = window
8242            .read_with(cx, |mw, _| mw.workspace().clone())
8243            .unwrap();
8244        let cx = &mut VisualTestContext::from_window(window.into(), cx);
8245
8246        cx.update(|_, cx| {
8247            let settings = *ProjectPanelSettings::get_global(cx);
8248            ProjectPanelSettings::override_global(
8249                ProjectPanelSettings {
8250                    hide_root: false,
8251                    ..settings
8252                },
8253                cx,
8254            );
8255        });
8256
8257        let panel = workspace.update_in(cx, ProjectPanel::new);
8258        cx.run_until_parked();
8259
8260        #[rustfmt::skip]
8261        assert_eq!(
8262            visible_entries_as_strings(&panel, 0..10, cx),
8263            &[
8264                "v root1",
8265                "    > dir1",
8266                "    > dir2",
8267                "      file4.txt",
8268            ],
8269            "With hide_root=false and single worktree, root should be visible"
8270        );
8271    }
8272
8273    // Test 2: Single worktree with hide_root = true
8274    {
8275        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
8276        let window =
8277            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8278        let workspace = window
8279            .read_with(cx, |mw, _| mw.workspace().clone())
8280            .unwrap();
8281        let cx = &mut VisualTestContext::from_window(window.into(), cx);
8282
8283        // Set hide_root to true
8284        cx.update(|_, cx| {
8285            let settings = *ProjectPanelSettings::get_global(cx);
8286            ProjectPanelSettings::override_global(
8287                ProjectPanelSettings {
8288                    hide_root: true,
8289                    ..settings
8290                },
8291                cx,
8292            );
8293        });
8294
8295        let panel = workspace.update_in(cx, ProjectPanel::new);
8296        cx.run_until_parked();
8297
8298        assert_eq!(
8299            visible_entries_as_strings(&panel, 0..10, cx),
8300            &["> dir1", "> dir2", "  file4.txt",],
8301            "With hide_root=true and single worktree, root should be hidden"
8302        );
8303
8304        // Test expanding directories still works without root
8305        toggle_expand_dir(&panel, "root1/dir1", cx);
8306        assert_eq!(
8307            visible_entries_as_strings(&panel, 0..10, cx),
8308            &[
8309                "v dir1  <== selected",
8310                "      file1.txt",
8311                "      file2.txt",
8312                "> dir2",
8313                "  file4.txt",
8314            ],
8315            "Should be able to expand directories even when root is hidden"
8316        );
8317    }
8318
8319    // Test 3: Multiple worktrees with hide_root = true
8320    {
8321        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
8322        let window =
8323            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8324        let workspace = window
8325            .read_with(cx, |mw, _| mw.workspace().clone())
8326            .unwrap();
8327        let cx = &mut VisualTestContext::from_window(window.into(), cx);
8328
8329        // Set hide_root to true
8330        cx.update(|_, cx| {
8331            let settings = *ProjectPanelSettings::get_global(cx);
8332            ProjectPanelSettings::override_global(
8333                ProjectPanelSettings {
8334                    hide_root: true,
8335                    ..settings
8336                },
8337                cx,
8338            );
8339        });
8340
8341        let panel = workspace.update_in(cx, ProjectPanel::new);
8342        cx.run_until_parked();
8343
8344        assert_eq!(
8345            visible_entries_as_strings(&panel, 0..10, cx),
8346            &[
8347                "v root1",
8348                "    > dir1",
8349                "    > dir2",
8350                "      file4.txt",
8351                "v root2",
8352                "    > dir3",
8353                "      file6.txt",
8354            ],
8355            "With hide_root=true and multiple worktrees, roots should still be visible"
8356        );
8357    }
8358
8359    // Test 4: Multiple worktrees with hide_root = false
8360    {
8361        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
8362        let window =
8363            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8364        let workspace = window
8365            .read_with(cx, |mw, _| mw.workspace().clone())
8366            .unwrap();
8367        let cx = &mut VisualTestContext::from_window(window.into(), cx);
8368
8369        cx.update(|_, cx| {
8370            let settings = *ProjectPanelSettings::get_global(cx);
8371            ProjectPanelSettings::override_global(
8372                ProjectPanelSettings {
8373                    hide_root: false,
8374                    ..settings
8375                },
8376                cx,
8377            );
8378        });
8379
8380        let panel = workspace.update_in(cx, ProjectPanel::new);
8381        cx.run_until_parked();
8382
8383        assert_eq!(
8384            visible_entries_as_strings(&panel, 0..10, cx),
8385            &[
8386                "v root1",
8387                "    > dir1",
8388                "    > dir2",
8389                "      file4.txt",
8390                "v root2",
8391                "    > dir3",
8392                "      file6.txt",
8393            ],
8394            "With hide_root=false and multiple worktrees, roots should be visible"
8395        );
8396    }
8397}
8398
8399#[gpui::test]
8400async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
8401    init_test_with_editor(cx);
8402
8403    let fs = FakeFs::new(cx.executor());
8404    fs.insert_tree(
8405        "/root",
8406        json!({
8407            "file1.txt": "content of file1",
8408            "file2.txt": "content of file2",
8409            "dir1": {
8410                "file3.txt": "content of file3"
8411            }
8412        }),
8413    )
8414    .await;
8415
8416    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8417    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8418    let workspace = window
8419        .read_with(cx, |mw, _| mw.workspace().clone())
8420        .unwrap();
8421    let cx = &mut VisualTestContext::from_window(window.into(), cx);
8422    let panel = workspace.update_in(cx, ProjectPanel::new);
8423    cx.run_until_parked();
8424
8425    let file1_path = "root/file1.txt";
8426    let file2_path = "root/file2.txt";
8427    select_path_with_mark(&panel, file1_path, cx);
8428    select_path_with_mark(&panel, file2_path, cx);
8429
8430    panel.update_in(cx, |panel, window, cx| {
8431        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
8432    });
8433    cx.executor().run_until_parked();
8434
8435    workspace.update_in(cx, |workspace, _, cx| {
8436        let active_items = workspace
8437            .panes()
8438            .iter()
8439            .filter_map(|pane| pane.read(cx).active_item())
8440            .collect::<Vec<_>>();
8441        assert_eq!(active_items.len(), 1);
8442        let diff_view = active_items
8443            .into_iter()
8444            .next()
8445            .unwrap()
8446            .downcast::<FileDiffView>()
8447            .expect("Open item should be an FileDiffView");
8448        assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
8449        assert_eq!(
8450            diff_view.tab_tooltip_text(cx).unwrap(),
8451            format!(
8452                "{}{}",
8453                rel_path(file1_path).display(PathStyle::local()),
8454                rel_path(file2_path).display(PathStyle::local())
8455            )
8456        );
8457    });
8458
8459    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
8460    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
8461    let worktree_id = panel.update(cx, |panel, cx| {
8462        panel
8463            .project
8464            .read(cx)
8465            .worktrees(cx)
8466            .next()
8467            .unwrap()
8468            .read(cx)
8469            .id()
8470    });
8471
8472    let expected_entries = [
8473        SelectedEntry {
8474            worktree_id,
8475            entry_id: file1_entry_id,
8476        },
8477        SelectedEntry {
8478            worktree_id,
8479            entry_id: file2_entry_id,
8480        },
8481    ];
8482    panel.update(cx, |panel, _cx| {
8483        assert_eq!(
8484            &panel.marked_entries, &expected_entries,
8485            "Should keep marked entries after comparison"
8486        );
8487    });
8488
8489    panel.update(cx, |panel, cx| {
8490        panel.project.update(cx, |_, cx| {
8491            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
8492        })
8493    });
8494
8495    panel.update(cx, |panel, _cx| {
8496        assert_eq!(
8497            &panel.marked_entries, &expected_entries,
8498            "Marked entries should persist after focusing back on the project panel"
8499        );
8500    });
8501}
8502
8503#[gpui::test]
8504async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
8505    init_test_with_editor(cx);
8506
8507    let fs = FakeFs::new(cx.executor());
8508    fs.insert_tree(
8509        "/root",
8510        json!({
8511            "file1.txt": "content of file1",
8512            "file2.txt": "content of file2",
8513            "dir1": {},
8514            "dir2": {
8515                "file3.txt": "content of file3"
8516            }
8517        }),
8518    )
8519    .await;
8520
8521    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8522    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8523    let workspace = window
8524        .read_with(cx, |mw, _| mw.workspace().clone())
8525        .unwrap();
8526    let cx = &mut VisualTestContext::from_window(window.into(), cx);
8527    let panel = workspace.update_in(cx, ProjectPanel::new);
8528    cx.run_until_parked();
8529
8530    // Test 1: When only one file is selected, there should be no compare option
8531    select_path(&panel, "root/file1.txt", cx);
8532
8533    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
8534    assert_eq!(
8535        selected_files, None,
8536        "Should not have compare option when only one file is selected"
8537    );
8538
8539    // Test 2: When multiple files are selected, there should be a compare option
8540    select_path_with_mark(&panel, "root/file1.txt", cx);
8541    select_path_with_mark(&panel, "root/file2.txt", cx);
8542
8543    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
8544    assert!(
8545        selected_files.is_some(),
8546        "Should have files selected for comparison"
8547    );
8548    if let Some((file1, file2)) = selected_files {
8549        assert!(
8550            file1.to_string_lossy().ends_with("file1.txt")
8551                && file2.to_string_lossy().ends_with("file2.txt"),
8552            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
8553        );
8554    }
8555
8556    // Test 3: Selecting a directory shouldn't count as a comparable file
8557    select_path_with_mark(&panel, "root/dir1", cx);
8558
8559    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
8560    assert!(
8561        selected_files.is_some(),
8562        "Directory selection should not affect comparable files"
8563    );
8564    if let Some((file1, file2)) = selected_files {
8565        assert!(
8566            file1.to_string_lossy().ends_with("file1.txt")
8567                && file2.to_string_lossy().ends_with("file2.txt"),
8568            "Selecting a directory should not affect the number of comparable files"
8569        );
8570    }
8571
8572    // Test 4: Selecting one more file
8573    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
8574
8575    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
8576    assert!(
8577        selected_files.is_some(),
8578        "Directory selection should not affect comparable files"
8579    );
8580    if let Some((file1, file2)) = selected_files {
8581        assert!(
8582            file1.to_string_lossy().ends_with("file2.txt")
8583                && file2.to_string_lossy().ends_with("file3.txt"),
8584            "Selecting a directory should not affect the number of comparable files"
8585        );
8586    }
8587}
8588
8589#[gpui::test]
8590async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
8591    init_test(cx);
8592
8593    let fs = FakeFs::new(cx.executor());
8594    fs.insert_tree(
8595        "/root",
8596        json!({
8597            ".hidden-file.txt": "hidden file content",
8598            "visible-file.txt": "visible file content",
8599            ".hidden-parent-dir": {
8600                "nested-dir": {
8601                    "file.txt": "file content",
8602                }
8603            },
8604            "visible-dir": {
8605                "file-in-visible.txt": "file content",
8606                "nested": {
8607                    ".hidden-nested-dir": {
8608                        ".double-hidden-dir": {
8609                            "deep-file-1.txt": "deep content 1",
8610                            "deep-file-2.txt": "deep content 2"
8611                        },
8612                        "hidden-nested-file-1.txt": "hidden nested 1",
8613                        "hidden-nested-file-2.txt": "hidden nested 2"
8614                    },
8615                    "visible-nested-file.txt": "visible nested content"
8616                }
8617            }
8618        }),
8619    )
8620    .await;
8621
8622    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8623    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8624    let workspace = window
8625        .read_with(cx, |mw, _| mw.workspace().clone())
8626        .unwrap();
8627    let cx = &mut VisualTestContext::from_window(window.into(), cx);
8628
8629    cx.update(|_, cx| {
8630        let settings = *ProjectPanelSettings::get_global(cx);
8631        ProjectPanelSettings::override_global(
8632            ProjectPanelSettings {
8633                hide_hidden: false,
8634                ..settings
8635            },
8636            cx,
8637        );
8638    });
8639
8640    let panel = workspace.update_in(cx, ProjectPanel::new);
8641    cx.run_until_parked();
8642
8643    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
8644    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
8645    toggle_expand_dir(&panel, "root/visible-dir", cx);
8646    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
8647    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
8648    toggle_expand_dir(
8649        &panel,
8650        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
8651        cx,
8652    );
8653
8654    let expanded = [
8655        "v root",
8656        "    v .hidden-parent-dir",
8657        "        v nested-dir",
8658        "              file.txt",
8659        "    v visible-dir",
8660        "        v nested",
8661        "            v .hidden-nested-dir",
8662        "                v .double-hidden-dir  <== selected",
8663        "                      deep-file-1.txt",
8664        "                      deep-file-2.txt",
8665        "                  hidden-nested-file-1.txt",
8666        "                  hidden-nested-file-2.txt",
8667        "              visible-nested-file.txt",
8668        "          file-in-visible.txt",
8669        "      .hidden-file.txt",
8670        "      visible-file.txt",
8671    ];
8672
8673    assert_eq!(
8674        visible_entries_as_strings(&panel, 0..30, cx),
8675        &expanded,
8676        "With hide_hidden=false, contents of hidden nested directory should be visible"
8677    );
8678
8679    cx.update(|_, cx| {
8680        let settings = *ProjectPanelSettings::get_global(cx);
8681        ProjectPanelSettings::override_global(
8682            ProjectPanelSettings {
8683                hide_hidden: true,
8684                ..settings
8685            },
8686            cx,
8687        );
8688    });
8689
8690    panel.update_in(cx, |panel, window, cx| {
8691        panel.update_visible_entries(None, false, false, window, cx);
8692    });
8693    cx.run_until_parked();
8694
8695    assert_eq!(
8696        visible_entries_as_strings(&panel, 0..30, cx),
8697        &[
8698            "v root",
8699            "    v visible-dir",
8700            "        v nested",
8701            "              visible-nested-file.txt",
8702            "          file-in-visible.txt",
8703            "      visible-file.txt",
8704        ],
8705        "With hide_hidden=false, contents of hidden nested directory should be visible"
8706    );
8707
8708    panel.update_in(cx, |panel, window, cx| {
8709        let settings = *ProjectPanelSettings::get_global(cx);
8710        ProjectPanelSettings::override_global(
8711            ProjectPanelSettings {
8712                hide_hidden: false,
8713                ..settings
8714            },
8715            cx,
8716        );
8717        panel.update_visible_entries(None, false, false, window, cx);
8718    });
8719    cx.run_until_parked();
8720
8721    assert_eq!(
8722        visible_entries_as_strings(&panel, 0..30, cx),
8723        &expanded,
8724        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
8725    );
8726}
8727
8728fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
8729    let path = rel_path(path);
8730    panel.update_in(cx, |panel, window, cx| {
8731        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
8732            let worktree = worktree.read(cx);
8733            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
8734                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
8735                panel.update_visible_entries(
8736                    Some((worktree.id(), entry_id)),
8737                    false,
8738                    false,
8739                    window,
8740                    cx,
8741                );
8742                return;
8743            }
8744        }
8745        panic!("no worktree for path {:?}", path);
8746    });
8747    cx.run_until_parked();
8748}
8749
8750fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
8751    let path = rel_path(path);
8752    panel.update(cx, |panel, cx| {
8753        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
8754            let worktree = worktree.read(cx);
8755            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
8756                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
8757                let entry = crate::SelectedEntry {
8758                    worktree_id: worktree.id(),
8759                    entry_id,
8760                };
8761                if !panel.marked_entries.contains(&entry) {
8762                    panel.marked_entries.push(entry);
8763                }
8764                panel.selection = Some(entry);
8765                return;
8766            }
8767        }
8768        panic!("no worktree for path {:?}", path);
8769    });
8770}
8771
8772/// `leaf_path` is the full path to the leaf entry (e.g., "root/a/b/c")
8773/// `active_ancestor_path` is the path to the folded component that should be active.
8774fn select_folded_path_with_mark(
8775    panel: &Entity<ProjectPanel>,
8776    leaf_path: &str,
8777    active_ancestor_path: &str,
8778    cx: &mut VisualTestContext,
8779) {
8780    select_path_with_mark(panel, leaf_path, cx);
8781    set_folded_active_ancestor(panel, leaf_path, active_ancestor_path, cx);
8782}
8783
8784fn set_folded_active_ancestor(
8785    panel: &Entity<ProjectPanel>,
8786    leaf_path: &str,
8787    active_ancestor_path: &str,
8788    cx: &mut VisualTestContext,
8789) {
8790    let leaf_path = rel_path(leaf_path);
8791    let active_ancestor_path = rel_path(active_ancestor_path);
8792    panel.update(cx, |panel, cx| {
8793        let mut leaf_entry_id = None;
8794        let mut target_entry_id = None;
8795
8796        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
8797            let worktree = worktree.read(cx);
8798            if let Ok(relative_path) = leaf_path.strip_prefix(worktree.root_name()) {
8799                leaf_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
8800            }
8801            if let Ok(relative_path) = active_ancestor_path.strip_prefix(worktree.root_name()) {
8802                target_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
8803            }
8804        }
8805
8806        let leaf_entry_id =
8807            leaf_entry_id.unwrap_or_else(|| panic!("no entry for leaf path {leaf_path:?}"));
8808        let target_entry_id = target_entry_id
8809            .unwrap_or_else(|| panic!("no entry for active path {active_ancestor_path:?}"));
8810        let folded_ancestors = panel
8811            .state
8812            .ancestors
8813            .get_mut(&leaf_entry_id)
8814            .unwrap_or_else(|| panic!("leaf path {leaf_path:?} should be folded"));
8815        let ancestor_ids = folded_ancestors.ancestors.clone();
8816
8817        let mut depth_for_target = None;
8818        for depth in 0..ancestor_ids.len() {
8819            let resolved_entry_id = if depth == 0 {
8820                leaf_entry_id
8821            } else {
8822                ancestor_ids.get(depth).copied().unwrap_or(leaf_entry_id)
8823            };
8824            if resolved_entry_id == target_entry_id {
8825                depth_for_target = Some(depth);
8826                break;
8827            }
8828        }
8829
8830        folded_ancestors.current_ancestor_depth = depth_for_target.unwrap_or_else(|| {
8831            panic!(
8832                "active path {active_ancestor_path:?} is not part of folded ancestors {ancestor_ids:?}"
8833            )
8834        });
8835    });
8836}
8837
8838fn drag_selection_to(
8839    panel: &Entity<ProjectPanel>,
8840    target_path: &str,
8841    is_file: bool,
8842    cx: &mut VisualTestContext,
8843) {
8844    let target_entry = find_project_entry(panel, target_path, cx)
8845        .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
8846
8847    panel.update_in(cx, |panel, window, cx| {
8848        let selection = panel
8849            .selection
8850            .expect("a selection is required before dragging");
8851        let drag = DraggedSelection {
8852            active_selection: SelectedEntry {
8853                worktree_id: selection.worktree_id,
8854                entry_id: panel.resolve_entry(selection.entry_id),
8855            },
8856            marked_selections: Arc::from(panel.marked_entries.clone()),
8857        };
8858        panel.drag_onto(&drag, target_entry, is_file, window, cx);
8859    });
8860    cx.executor().run_until_parked();
8861}
8862
8863fn find_project_entry(
8864    panel: &Entity<ProjectPanel>,
8865    path: &str,
8866    cx: &mut VisualTestContext,
8867) -> Option<ProjectEntryId> {
8868    let path = rel_path(path);
8869    panel.update(cx, |panel, cx| {
8870        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
8871            let worktree = worktree.read(cx);
8872            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
8873                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
8874            }
8875        }
8876        panic!("no worktree for path {path:?}");
8877    })
8878}
8879
8880fn visible_entries_as_strings(
8881    panel: &Entity<ProjectPanel>,
8882    range: Range<usize>,
8883    cx: &mut VisualTestContext,
8884) -> Vec<String> {
8885    let mut result = Vec::new();
8886    let mut project_entries = HashSet::default();
8887    let mut has_editor = false;
8888
8889    panel.update_in(cx, |panel, window, cx| {
8890        panel.for_each_visible_entry(range, window, cx, &mut |project_entry, details, _, _| {
8891            if details.is_editing {
8892                assert!(!has_editor, "duplicate editor entry");
8893                has_editor = true;
8894            } else {
8895                assert!(
8896                    project_entries.insert(project_entry),
8897                    "duplicate project entry {:?} {:?}",
8898                    project_entry,
8899                    details
8900                );
8901            }
8902
8903            let indent = "    ".repeat(details.depth);
8904            let icon = if details.kind.is_dir() {
8905                if details.is_expanded { "v " } else { "> " }
8906            } else {
8907                "  "
8908            };
8909            #[cfg(windows)]
8910            let filename = details.filename.replace("\\", "/");
8911            #[cfg(not(windows))]
8912            let filename = details.filename;
8913            let name = if details.is_editing {
8914                format!("[EDITOR: '{}']", filename)
8915            } else if details.is_processing {
8916                format!("[PROCESSING: '{}']", filename)
8917            } else {
8918                filename
8919            };
8920            let selected = if details.is_selected {
8921                "  <== selected"
8922            } else {
8923                ""
8924            };
8925            let marked = if details.is_marked {
8926                "  <== marked"
8927            } else {
8928                ""
8929            };
8930
8931            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
8932        });
8933    });
8934
8935    result
8936}
8937
8938/// Test that missing sort_mode field defaults to DirectoriesFirst
8939#[gpui::test]
8940async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
8941    init_test(cx);
8942
8943    // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
8944    let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
8945    assert_eq!(
8946        default_settings.sort_mode,
8947        settings::ProjectPanelSortMode::DirectoriesFirst,
8948        "sort_mode should default to DirectoriesFirst"
8949    );
8950}
8951
8952/// Test sort modes: DirectoriesFirst (default) vs Mixed
8953#[gpui::test]
8954async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
8955    init_test(cx);
8956
8957    let fs = FakeFs::new(cx.executor());
8958    fs.insert_tree(
8959        "/root",
8960        json!({
8961            "zebra.txt": "",
8962            "Apple": {},
8963            "banana.rs": "",
8964            "Carrot": {},
8965            "aardvark.txt": "",
8966        }),
8967    )
8968    .await;
8969
8970    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
8971    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
8972    let workspace = window
8973        .read_with(cx, |mw, _| mw.workspace().clone())
8974        .unwrap();
8975    let cx = &mut VisualTestContext::from_window(window.into(), cx);
8976    let panel = workspace.update_in(cx, ProjectPanel::new);
8977    cx.run_until_parked();
8978
8979    // Default sort mode should be DirectoriesFirst
8980    assert_eq!(
8981        visible_entries_as_strings(&panel, 0..50, cx),
8982        &[
8983            "v root",
8984            "    > Apple",
8985            "    > Carrot",
8986            "      aardvark.txt",
8987            "      banana.rs",
8988            "      zebra.txt",
8989        ]
8990    );
8991}
8992
8993#[gpui::test]
8994async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
8995    init_test(cx);
8996
8997    let fs = FakeFs::new(cx.executor());
8998    fs.insert_tree(
8999        "/root",
9000        json!({
9001            "Zebra.txt": "",
9002            "apple": {},
9003            "Banana.rs": "",
9004            "carrot": {},
9005            "Aardvark.txt": "",
9006        }),
9007    )
9008    .await;
9009
9010    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
9011    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
9012    let workspace = window
9013        .read_with(cx, |mw, _| mw.workspace().clone())
9014        .unwrap();
9015    let cx = &mut VisualTestContext::from_window(window.into(), cx);
9016
9017    // Switch to Mixed mode
9018    cx.update(|_, cx| {
9019        cx.update_global::<SettingsStore, _>(|store, cx| {
9020            store.update_user_settings(cx, |settings| {
9021                settings.project_panel.get_or_insert_default().sort_mode =
9022                    Some(settings::ProjectPanelSortMode::Mixed);
9023            });
9024        });
9025    });
9026
9027    let panel = workspace.update_in(cx, ProjectPanel::new);
9028    cx.run_until_parked();
9029
9030    // Mixed mode: case-insensitive sorting
9031    // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
9032    assert_eq!(
9033        visible_entries_as_strings(&panel, 0..50, cx),
9034        &[
9035            "v root",
9036            "      Aardvark.txt",
9037            "    > apple",
9038            "      Banana.rs",
9039            "    > carrot",
9040            "      Zebra.txt",
9041        ]
9042    );
9043}
9044
9045#[gpui::test]
9046async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
9047    init_test(cx);
9048
9049    let fs = FakeFs::new(cx.executor());
9050    fs.insert_tree(
9051        "/root",
9052        json!({
9053            "Zebra.txt": "",
9054            "apple": {},
9055            "Banana.rs": "",
9056            "carrot": {},
9057            "Aardvark.txt": "",
9058        }),
9059    )
9060    .await;
9061
9062    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
9063    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
9064    let workspace = window
9065        .read_with(cx, |mw, _| mw.workspace().clone())
9066        .unwrap();
9067    let cx = &mut VisualTestContext::from_window(window.into(), cx);
9068
9069    // Switch to FilesFirst mode
9070    cx.update(|_, cx| {
9071        cx.update_global::<SettingsStore, _>(|store, cx| {
9072            store.update_user_settings(cx, |settings| {
9073                settings.project_panel.get_or_insert_default().sort_mode =
9074                    Some(settings::ProjectPanelSortMode::FilesFirst);
9075            });
9076        });
9077    });
9078
9079    let panel = workspace.update_in(cx, ProjectPanel::new);
9080    cx.run_until_parked();
9081
9082    // FilesFirst mode: files first, then directories (both case-insensitive)
9083    assert_eq!(
9084        visible_entries_as_strings(&panel, 0..50, cx),
9085        &[
9086            "v root",
9087            "      Aardvark.txt",
9088            "      Banana.rs",
9089            "      Zebra.txt",
9090            "    > apple",
9091            "    > carrot",
9092        ]
9093    );
9094}
9095
9096#[gpui::test]
9097async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
9098    init_test(cx);
9099
9100    let fs = FakeFs::new(cx.executor());
9101    fs.insert_tree(
9102        "/root",
9103        json!({
9104            "file2.txt": "",
9105            "dir1": {},
9106            "file1.txt": "",
9107        }),
9108    )
9109    .await;
9110
9111    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
9112    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
9113    let workspace = window
9114        .read_with(cx, |mw, _| mw.workspace().clone())
9115        .unwrap();
9116    let cx = &mut VisualTestContext::from_window(window.into(), cx);
9117    let panel = workspace.update_in(cx, ProjectPanel::new);
9118    cx.run_until_parked();
9119
9120    // Initially DirectoriesFirst
9121    assert_eq!(
9122        visible_entries_as_strings(&panel, 0..50, cx),
9123        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
9124    );
9125
9126    // Toggle to Mixed
9127    cx.update(|_, cx| {
9128        cx.update_global::<SettingsStore, _>(|store, cx| {
9129            store.update_user_settings(cx, |settings| {
9130                settings.project_panel.get_or_insert_default().sort_mode =
9131                    Some(settings::ProjectPanelSortMode::Mixed);
9132            });
9133        });
9134    });
9135    cx.run_until_parked();
9136
9137    assert_eq!(
9138        visible_entries_as_strings(&panel, 0..50, cx),
9139        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
9140    );
9141
9142    // Toggle back to DirectoriesFirst
9143    cx.update(|_, cx| {
9144        cx.update_global::<SettingsStore, _>(|store, cx| {
9145            store.update_user_settings(cx, |settings| {
9146                settings.project_panel.get_or_insert_default().sort_mode =
9147                    Some(settings::ProjectPanelSortMode::DirectoriesFirst);
9148            });
9149        });
9150    });
9151    cx.run_until_parked();
9152
9153    assert_eq!(
9154        visible_entries_as_strings(&panel, 0..50, cx),
9155        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
9156    );
9157}
9158
9159#[gpui::test]
9160async fn test_ensure_temporary_folding_when_creating_in_different_nested_dirs(
9161    cx: &mut gpui::TestAppContext,
9162) {
9163    init_test(cx);
9164
9165    // parent: accept
9166    run_create_file_in_folded_path_case(
9167        "parent",
9168        "root1/parent",
9169        "file_in_parent.txt",
9170        &[
9171            "v root1",
9172            "    v parent",
9173            "        > subdir/child",
9174            "          [EDITOR: '']  <== selected",
9175        ],
9176        &[
9177            "v root1",
9178            "    v parent",
9179            "        > subdir/child",
9180            "          file_in_parent.txt  <== selected  <== marked",
9181        ],
9182        true,
9183        cx,
9184    )
9185    .await;
9186
9187    // parent: cancel
9188    run_create_file_in_folded_path_case(
9189        "parent",
9190        "root1/parent",
9191        "file_in_parent.txt",
9192        &[
9193            "v root1",
9194            "    v parent",
9195            "        > subdir/child",
9196            "          [EDITOR: '']  <== selected",
9197        ],
9198        &["v root1", "    > parent/subdir/child  <== selected"],
9199        false,
9200        cx,
9201    )
9202    .await;
9203
9204    // subdir: accept
9205    run_create_file_in_folded_path_case(
9206        "subdir",
9207        "root1/parent/subdir",
9208        "file_in_subdir.txt",
9209        &[
9210            "v root1",
9211            "    v parent/subdir",
9212            "        > child",
9213            "          [EDITOR: '']  <== selected",
9214        ],
9215        &[
9216            "v root1",
9217            "    v parent/subdir",
9218            "        > child",
9219            "          file_in_subdir.txt  <== selected  <== marked",
9220        ],
9221        true,
9222        cx,
9223    )
9224    .await;
9225
9226    // subdir: cancel
9227    run_create_file_in_folded_path_case(
9228        "subdir",
9229        "root1/parent/subdir",
9230        "file_in_subdir.txt",
9231        &[
9232            "v root1",
9233            "    v parent/subdir",
9234            "        > child",
9235            "          [EDITOR: '']  <== selected",
9236        ],
9237        &["v root1", "    > parent/subdir/child  <== selected"],
9238        false,
9239        cx,
9240    )
9241    .await;
9242
9243    // child: accept
9244    run_create_file_in_folded_path_case(
9245        "child",
9246        "root1/parent/subdir/child",
9247        "file_in_child.txt",
9248        &[
9249            "v root1",
9250            "    v parent/subdir/child",
9251            "          [EDITOR: '']  <== selected",
9252        ],
9253        &[
9254            "v root1",
9255            "    v parent/subdir/child",
9256            "          file_in_child.txt  <== selected  <== marked",
9257        ],
9258        true,
9259        cx,
9260    )
9261    .await;
9262
9263    // child: cancel
9264    run_create_file_in_folded_path_case(
9265        "child",
9266        "root1/parent/subdir/child",
9267        "file_in_child.txt",
9268        &[
9269            "v root1",
9270            "    v parent/subdir/child",
9271            "          [EDITOR: '']  <== selected",
9272        ],
9273        &["v root1", "    v parent/subdir/child  <== selected"],
9274        false,
9275        cx,
9276    )
9277    .await;
9278}
9279
9280#[gpui::test]
9281async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu(
9282    cx: &mut gpui::TestAppContext,
9283) {
9284    init_test(cx);
9285
9286    let fs = FakeFs::new(cx.executor());
9287    fs.insert_tree(
9288        "/root1",
9289        json!({
9290            "parent": {
9291                "subdir": {
9292                    "child": {},
9293                }
9294            }
9295        }),
9296    )
9297    .await;
9298
9299    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
9300    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
9301    let workspace = window
9302        .read_with(cx, |mw, _| mw.workspace().clone())
9303        .unwrap();
9304    let cx = &mut VisualTestContext::from_window(window.into(), cx);
9305
9306    let panel = workspace.update_in(cx, |workspace, window, cx| {
9307        let panel = ProjectPanel::new(workspace, window, cx);
9308        workspace.add_panel(panel.clone(), window, cx);
9309        panel
9310    });
9311
9312    cx.update(|_, cx| {
9313        let settings = *ProjectPanelSettings::get_global(cx);
9314        ProjectPanelSettings::override_global(
9315            ProjectPanelSettings {
9316                auto_fold_dirs: true,
9317                ..settings
9318            },
9319            cx,
9320        );
9321    });
9322
9323    panel.update_in(cx, |panel, window, cx| {
9324        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
9325    });
9326    cx.run_until_parked();
9327
9328    select_folded_path_with_mark(
9329        &panel,
9330        "root1/parent/subdir/child",
9331        "root1/parent/subdir",
9332        cx,
9333    );
9334    panel.update(cx, |panel, _| {
9335        panel.marked_entries.clear();
9336    });
9337
9338    let parent_entry_id = find_project_entry(&panel, "root1/parent", cx)
9339        .expect("parent directory should exist for this test");
9340    let subdir_entry_id = find_project_entry(&panel, "root1/parent/subdir", cx)
9341        .expect("subdir directory should exist for this test");
9342    let child_entry_id = find_project_entry(&panel, "root1/parent/subdir/child", cx)
9343        .expect("child directory should exist for this test");
9344
9345    panel.update(cx, |panel, _| {
9346        let selection = panel
9347            .selection
9348            .expect("leaf directory should be selected before creating a new entry");
9349        assert_eq!(
9350            selection.entry_id, child_entry_id,
9351            "initial selection should be the folded leaf entry"
9352        );
9353        assert_eq!(
9354            panel.resolve_entry(selection.entry_id),
9355            subdir_entry_id,
9356            "active folded component should start at subdir"
9357        );
9358    });
9359
9360    panel.update_in(cx, |panel, window, cx| {
9361        panel.deploy_context_menu(
9362            gpui::point(gpui::px(1.), gpui::px(1.)),
9363            child_entry_id,
9364            window,
9365            cx,
9366        );
9367        panel.new_file(&NewFile, window, cx);
9368    });
9369    cx.run_until_parked();
9370    panel.update_in(cx, |panel, window, cx| {
9371        assert!(panel.filename_editor.read(cx).is_focused(window));
9372    });
9373    cx.run_until_parked();
9374
9375    set_folded_active_ancestor(&panel, "root1/parent/subdir", "root1/parent", cx);
9376
9377    panel.update_in(cx, |panel, window, cx| {
9378        panel.deploy_context_menu(
9379            gpui::point(gpui::px(2.), gpui::px(2.)),
9380            subdir_entry_id,
9381            window,
9382            cx,
9383        );
9384    });
9385    cx.run_until_parked();
9386
9387    panel.update(cx, |panel, _| {
9388        assert!(
9389            panel.state.edit_state.is_none(),
9390            "opening another context menu should blur the filename editor and discard edit state"
9391        );
9392        let selection = panel
9393            .selection
9394            .expect("selection should restore to the previously focused leaf entry");
9395        assert_eq!(
9396            selection.entry_id, child_entry_id,
9397            "blur-driven cancellation should restore the previous leaf selection"
9398        );
9399        assert_eq!(
9400            panel.resolve_entry(selection.entry_id),
9401            parent_entry_id,
9402            "temporary unfolded pending state should preserve the active ancestor chosen before blur"
9403        );
9404    });
9405
9406    panel.update_in(cx, |panel, window, cx| {
9407        panel.new_file(&NewFile, window, cx);
9408    });
9409    cx.run_until_parked();
9410    assert_eq!(
9411        visible_entries_as_strings(&panel, 0..10, cx),
9412        &[
9413            "v root1",
9414            "    v parent",
9415            "        > subdir/child",
9416            "          [EDITOR: '']  <== selected",
9417        ],
9418        "new file after blur should use the preserved active ancestor"
9419    );
9420    panel.update(cx, |panel, _| {
9421        let edit_state = panel
9422            .state
9423            .edit_state
9424            .as_ref()
9425            .expect("new file should enter edit state");
9426        assert_eq!(
9427            edit_state.temporarily_unfolded,
9428            Some(parent_entry_id),
9429            "temporary unfolding should now target parent after restoring the active ancestor"
9430        );
9431    });
9432
9433    let file_name = "created_after_blur.txt";
9434    panel
9435        .update_in(cx, |panel, window, cx| {
9436            panel.filename_editor.update(cx, |editor, cx| {
9437                editor.set_text(file_name, window, cx);
9438            });
9439            panel.confirm_edit(true, window, cx).expect(
9440                "confirm_edit should start creation for the file created after blur transition",
9441            )
9442        })
9443        .await
9444        .expect("creating file after blur transition should succeed");
9445    cx.run_until_parked();
9446
9447    assert!(
9448        fs.is_file(Path::new("/root1/parent/created_after_blur.txt"))
9449            .await,
9450        "file should be created under parent after active ancestor is restored to parent"
9451    );
9452    assert!(
9453        !fs.is_file(Path::new("/root1/parent/subdir/created_after_blur.txt"))
9454            .await,
9455        "file should not be created under subdir when parent is the active ancestor"
9456    );
9457}
9458
9459async fn run_create_file_in_folded_path_case(
9460    case_name: &str,
9461    active_ancestor_path: &str,
9462    created_file_name: &str,
9463    expected_temporary_state: &[&str],
9464    expected_final_state: &[&str],
9465    accept_creation: bool,
9466    cx: &mut gpui::TestAppContext,
9467) {
9468    let expected_collapsed_state = &["v root1", "    > parent/subdir/child  <== selected"];
9469
9470    let fs = FakeFs::new(cx.executor());
9471    fs.insert_tree(
9472        "/root1",
9473        json!({
9474            "parent": {
9475                "subdir": {
9476                    "child": {},
9477                }
9478            }
9479        }),
9480    )
9481    .await;
9482
9483    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
9484    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
9485    let workspace = window
9486        .read_with(cx, |mw, _| mw.workspace().clone())
9487        .unwrap();
9488    let cx = &mut VisualTestContext::from_window(window.into(), cx);
9489
9490    let panel = workspace.update_in(cx, |workspace, window, cx| {
9491        let panel = ProjectPanel::new(workspace, window, cx);
9492        workspace.add_panel(panel.clone(), window, cx);
9493        panel
9494    });
9495
9496    cx.update(|_, cx| {
9497        let settings = *ProjectPanelSettings::get_global(cx);
9498        ProjectPanelSettings::override_global(
9499            ProjectPanelSettings {
9500                auto_fold_dirs: true,
9501                ..settings
9502            },
9503            cx,
9504        );
9505    });
9506
9507    panel.update_in(cx, |panel, window, cx| {
9508        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
9509    });
9510    cx.run_until_parked();
9511
9512    select_folded_path_with_mark(
9513        &panel,
9514        "root1/parent/subdir/child",
9515        active_ancestor_path,
9516        cx,
9517    );
9518    panel.update(cx, |panel, _| {
9519        panel.marked_entries.clear();
9520    });
9521
9522    assert_eq!(
9523        visible_entries_as_strings(&panel, 0..10, cx),
9524        expected_collapsed_state,
9525        "case '{}' should start from a folded state",
9526        case_name
9527    );
9528
9529    panel.update_in(cx, |panel, window, cx| {
9530        panel.new_file(&NewFile, window, cx);
9531    });
9532    cx.run_until_parked();
9533    panel.update_in(cx, |panel, window, cx| {
9534        assert!(panel.filename_editor.read(cx).is_focused(window));
9535    });
9536    cx.run_until_parked();
9537    assert_eq!(
9538        visible_entries_as_strings(&panel, 0..10, cx),
9539        expected_temporary_state,
9540        "case '{}' ({}) should temporarily unfold the active ancestor while editing",
9541        case_name,
9542        if accept_creation { "accept" } else { "cancel" }
9543    );
9544
9545    let relative_directory = active_ancestor_path
9546        .strip_prefix("root1/")
9547        .expect("active_ancestor_path should start with root1/");
9548    let created_file_path = PathBuf::from("/root1")
9549        .join(relative_directory)
9550        .join(created_file_name);
9551
9552    if accept_creation {
9553        panel
9554            .update_in(cx, |panel, window, cx| {
9555                panel.filename_editor.update(cx, |editor, cx| {
9556                    editor.set_text(created_file_name, window, cx);
9557                });
9558                panel.confirm_edit(true, window, cx).unwrap()
9559            })
9560            .await
9561            .unwrap();
9562        cx.run_until_parked();
9563
9564        assert_eq!(
9565            visible_entries_as_strings(&panel, 0..10, cx),
9566            expected_final_state,
9567            "case '{}' should keep the newly created file selected and marked after accept",
9568            case_name
9569        );
9570        assert!(
9571            fs.is_file(created_file_path.as_path()).await,
9572            "case '{}' should create file '{}'",
9573            case_name,
9574            created_file_path.display()
9575        );
9576    } else {
9577        panel.update_in(cx, |panel, window, cx| {
9578            panel.cancel(&Cancel, window, cx);
9579        });
9580        cx.run_until_parked();
9581
9582        assert_eq!(
9583            visible_entries_as_strings(&panel, 0..10, cx),
9584            expected_final_state,
9585            "case '{}' should keep the expected panel state after cancel",
9586            case_name
9587        );
9588        assert!(
9589            !fs.is_file(created_file_path.as_path()).await,
9590            "case '{}' should not create a file after cancel",
9591            case_name
9592        );
9593    }
9594}
9595
9596fn init_test(cx: &mut TestAppContext) {
9597    cx.update(|cx| {
9598        let settings_store = SettingsStore::test(cx);
9599        cx.set_global(settings_store);
9600        theme::init(theme::LoadThemes::JustBase, cx);
9601        crate::init(cx);
9602
9603        cx.update_global::<SettingsStore, _>(|store, cx| {
9604            store.update_user_settings(cx, |settings| {
9605                settings
9606                    .project_panel
9607                    .get_or_insert_default()
9608                    .auto_fold_dirs = Some(false);
9609                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
9610            });
9611        });
9612    });
9613}
9614
9615fn init_test_with_editor(cx: &mut TestAppContext) {
9616    cx.update(|cx| {
9617        let app_state = AppState::test(cx);
9618        theme::init(theme::LoadThemes::JustBase, cx);
9619        editor::init(cx);
9620        crate::init(cx);
9621        workspace::init(app_state, cx);
9622
9623        cx.update_global::<SettingsStore, _>(|store, cx| {
9624            store.update_user_settings(cx, |settings| {
9625                settings
9626                    .project_panel
9627                    .get_or_insert_default()
9628                    .auto_fold_dirs = Some(false);
9629                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
9630            });
9631        });
9632    });
9633}
9634
9635fn set_auto_open_settings(
9636    cx: &mut TestAppContext,
9637    auto_open_settings: ProjectPanelAutoOpenSettings,
9638) {
9639    cx.update(|cx| {
9640        cx.update_global::<SettingsStore, _>(|store, cx| {
9641            store.update_user_settings(cx, |settings| {
9642                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
9643            });
9644        })
9645    });
9646}
9647
9648fn ensure_single_file_is_opened(
9649    workspace: &Entity<Workspace>,
9650    expected_path: &str,
9651    cx: &mut VisualTestContext,
9652) {
9653    workspace.update_in(cx, |workspace, _, cx| {
9654        let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
9655        assert_eq!(worktrees.len(), 1);
9656        let worktree_id = worktrees[0].read(cx).id();
9657
9658        let open_project_paths = workspace
9659            .panes()
9660            .iter()
9661            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
9662            .collect::<Vec<_>>();
9663        assert_eq!(
9664            open_project_paths,
9665            vec![ProjectPath {
9666                worktree_id,
9667                path: Arc::from(rel_path(expected_path))
9668            }],
9669            "Should have opened file, selected in project panel"
9670        );
9671    });
9672}
9673
9674fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
9675    assert!(
9676        !cx.has_pending_prompt(),
9677        "Should have no prompts before the deletion"
9678    );
9679    panel.update_in(cx, |panel, window, cx| {
9680        panel.delete(&Delete { skip_prompt: false }, window, cx)
9681    });
9682    assert!(
9683        cx.has_pending_prompt(),
9684        "Should have a prompt after the deletion"
9685    );
9686    cx.simulate_prompt_answer("Delete");
9687    assert!(
9688        !cx.has_pending_prompt(),
9689        "Should have no prompts after prompt was replied to"
9690    );
9691    cx.executor().run_until_parked();
9692}
9693
9694fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
9695    assert!(
9696        !cx.has_pending_prompt(),
9697        "Should have no prompts before the deletion"
9698    );
9699    panel.update_in(cx, |panel, window, cx| {
9700        panel.delete(&Delete { skip_prompt: true }, window, cx)
9701    });
9702    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
9703    cx.executor().run_until_parked();
9704}
9705
9706fn ensure_no_open_items_and_panes(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) {
9707    assert!(
9708        !cx.has_pending_prompt(),
9709        "Should have no prompts after deletion operation closes the file"
9710    );
9711    workspace.update_in(cx, |workspace, _window, cx| {
9712        let open_project_paths = workspace
9713            .panes()
9714            .iter()
9715            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
9716            .collect::<Vec<_>>();
9717        assert!(
9718            open_project_paths.is_empty(),
9719            "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
9720        );
9721    });
9722}
9723
9724struct TestProjectItemView {
9725    focus_handle: FocusHandle,
9726    path: ProjectPath,
9727}
9728
9729struct TestProjectItem {
9730    path: ProjectPath,
9731}
9732
9733impl project::ProjectItem for TestProjectItem {
9734    fn try_open(
9735        _project: &Entity<Project>,
9736        path: &ProjectPath,
9737        cx: &mut App,
9738    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9739        let path = path.clone();
9740        Some(cx.spawn(async move |cx| Ok(cx.new(|_| Self { path }))))
9741    }
9742
9743    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9744        None
9745    }
9746
9747    fn project_path(&self, _: &App) -> Option<ProjectPath> {
9748        Some(self.path.clone())
9749    }
9750
9751    fn is_dirty(&self) -> bool {
9752        false
9753    }
9754}
9755
9756impl ProjectItem for TestProjectItemView {
9757    type Item = TestProjectItem;
9758
9759    fn for_project_item(
9760        _: Entity<Project>,
9761        _: Option<&Pane>,
9762        project_item: Entity<Self::Item>,
9763        _: &mut Window,
9764        cx: &mut Context<Self>,
9765    ) -> Self
9766    where
9767        Self: Sized,
9768    {
9769        Self {
9770            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
9771            focus_handle: cx.focus_handle(),
9772        }
9773    }
9774}
9775
9776impl Item for TestProjectItemView {
9777    type Event = ();
9778
9779    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9780        "Test".into()
9781    }
9782}
9783
9784impl EventEmitter<()> for TestProjectItemView {}
9785
9786impl Focusable for TestProjectItemView {
9787    fn focus_handle(&self, _: &App) -> FocusHandle {
9788        self.focus_handle.clone()
9789    }
9790}
9791
9792impl Render for TestProjectItemView {
9793    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9794        Empty
9795    }
9796}