project_panel_tests.rs

   1use super::*;
   2use collections::HashSet;
   3use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
   4use pretty_assertions::assert_eq;
   5use project::FakeFs;
   6use serde_json::json;
   7use settings::SettingsStore;
   8use std::path::{Path, PathBuf};
   9use util::{path, paths::PathStyle, rel_path::rel_path};
  10use workspace::{
  11    AppState, ItemHandle, Pane,
  12    item::{Item, ProjectItem},
  13    register_project_item,
  14};
  15
  16#[gpui::test]
  17async fn test_visible_list(cx: &mut gpui::TestAppContext) {
  18    init_test(cx);
  19
  20    let fs = FakeFs::new(cx.executor());
  21    fs.insert_tree(
  22        "/root1",
  23        json!({
  24            ".dockerignore": "",
  25            ".git": {
  26                "HEAD": "",
  27            },
  28            "a": {
  29                "0": { "q": "", "r": "", "s": "" },
  30                "1": { "t": "", "u": "" },
  31                "2": { "v": "", "w": "", "x": "", "y": "" },
  32            },
  33            "b": {
  34                "3": { "Q": "" },
  35                "4": { "R": "", "S": "", "T": "", "U": "" },
  36            },
  37            "C": {
  38                "5": {},
  39                "6": { "V": "", "W": "" },
  40                "7": { "X": "" },
  41                "8": { "Y": {}, "Z": "" }
  42            }
  43        }),
  44    )
  45    .await;
  46    fs.insert_tree(
  47        "/root2",
  48        json!({
  49            "d": {
  50                "9": ""
  51            },
  52            "e": {}
  53        }),
  54    )
  55    .await;
  56
  57    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  58    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
  59    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  60    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
  61    cx.run_until_parked();
  62    assert_eq!(
  63        visible_entries_as_strings(&panel, 0..50, cx),
  64        &[
  65            "v root1",
  66            "    > .git",
  67            "    > a",
  68            "    > b",
  69            "    > C",
  70            "      .dockerignore",
  71            "v root2",
  72            "    > d",
  73            "    > e",
  74        ]
  75    );
  76
  77    toggle_expand_dir(&panel, "root1/b", cx);
  78    assert_eq!(
  79        visible_entries_as_strings(&panel, 0..50, cx),
  80        &[
  81            "v root1",
  82            "    > .git",
  83            "    > a",
  84            "    v b  <== selected",
  85            "        > 3",
  86            "        > 4",
  87            "    > C",
  88            "      .dockerignore",
  89            "v root2",
  90            "    > d",
  91            "    > e",
  92        ]
  93    );
  94
  95    assert_eq!(
  96        visible_entries_as_strings(&panel, 6..9, cx),
  97        &[
  98            //
  99            "    > C",
 100            "      .dockerignore",
 101            "v root2",
 102        ]
 103    );
 104}
 105
 106#[gpui::test]
 107async fn test_opening_file(cx: &mut gpui::TestAppContext) {
 108    init_test_with_editor(cx);
 109
 110    let fs = FakeFs::new(cx.executor());
 111    fs.insert_tree(
 112        path!("/src"),
 113        json!({
 114            "test": {
 115                "first.rs": "// First Rust file",
 116                "second.rs": "// Second Rust file",
 117                "third.rs": "// Third Rust file",
 118            }
 119        }),
 120    )
 121    .await;
 122
 123    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
 124    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 125    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 126    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 127    cx.run_until_parked();
 128
 129    toggle_expand_dir(&panel, "src/test", cx);
 130    select_path(&panel, "src/test/first.rs", cx);
 131    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 132    cx.executor().run_until_parked();
 133    assert_eq!(
 134        visible_entries_as_strings(&panel, 0..10, cx),
 135        &[
 136            "v src",
 137            "    v test",
 138            "          first.rs  <== selected  <== marked",
 139            "          second.rs",
 140            "          third.rs"
 141        ]
 142    );
 143    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
 144
 145    select_path(&panel, "src/test/second.rs", cx);
 146    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 147    cx.executor().run_until_parked();
 148    assert_eq!(
 149        visible_entries_as_strings(&panel, 0..10, cx),
 150        &[
 151            "v src",
 152            "    v test",
 153            "          first.rs",
 154            "          second.rs  <== selected  <== marked",
 155            "          third.rs"
 156        ]
 157    );
 158    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
 159}
 160
 161#[gpui::test]
 162async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
 163    init_test(cx);
 164    cx.update(|cx| {
 165        cx.update_global::<SettingsStore, _>(|store, cx| {
 166            store.update_user_settings(cx, |settings| {
 167                settings.project.worktree.file_scan_exclusions =
 168                    Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
 169            });
 170        });
 171    });
 172
 173    let fs = FakeFs::new(cx.background_executor.clone());
 174    fs.insert_tree(
 175        "/root1",
 176        json!({
 177            ".dockerignore": "",
 178            ".git": {
 179                "HEAD": "",
 180            },
 181            "a": {
 182                "0": { "q": "", "r": "", "s": "" },
 183                "1": { "t": "", "u": "" },
 184                "2": { "v": "", "w": "", "x": "", "y": "" },
 185            },
 186            "b": {
 187                "3": { "Q": "" },
 188                "4": { "R": "", "S": "", "T": "", "U": "" },
 189            },
 190            "C": {
 191                "5": {},
 192                "6": { "V": "", "W": "" },
 193                "7": { "X": "" },
 194                "8": { "Y": {}, "Z": "" }
 195            }
 196        }),
 197    )
 198    .await;
 199    fs.insert_tree(
 200        "/root2",
 201        json!({
 202            "d": {
 203                "4": ""
 204            },
 205            "e": {}
 206        }),
 207    )
 208    .await;
 209
 210    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 211    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 212    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 213    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 214    cx.run_until_parked();
 215    assert_eq!(
 216        visible_entries_as_strings(&panel, 0..50, cx),
 217        &[
 218            "v root1",
 219            "    > a",
 220            "    > b",
 221            "    > C",
 222            "      .dockerignore",
 223            "v root2",
 224            "    > d",
 225            "    > e",
 226        ]
 227    );
 228
 229    toggle_expand_dir(&panel, "root1/b", cx);
 230    assert_eq!(
 231        visible_entries_as_strings(&panel, 0..50, cx),
 232        &[
 233            "v root1",
 234            "    > a",
 235            "    v b  <== selected",
 236            "        > 3",
 237            "    > C",
 238            "      .dockerignore",
 239            "v root2",
 240            "    > d",
 241            "    > e",
 242        ]
 243    );
 244
 245    toggle_expand_dir(&panel, "root2/d", cx);
 246    assert_eq!(
 247        visible_entries_as_strings(&panel, 0..50, cx),
 248        &[
 249            "v root1",
 250            "    > a",
 251            "    v b",
 252            "        > 3",
 253            "    > C",
 254            "      .dockerignore",
 255            "v root2",
 256            "    v d  <== selected",
 257            "    > e",
 258        ]
 259    );
 260
 261    toggle_expand_dir(&panel, "root2/e", cx);
 262    assert_eq!(
 263        visible_entries_as_strings(&panel, 0..50, cx),
 264        &[
 265            "v root1",
 266            "    > a",
 267            "    v b",
 268            "        > 3",
 269            "    > C",
 270            "      .dockerignore",
 271            "v root2",
 272            "    v d",
 273            "    v e  <== selected",
 274        ]
 275    );
 276}
 277
 278#[gpui::test]
 279async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
 280    init_test(cx);
 281
 282    let fs = FakeFs::new(cx.executor());
 283    fs.insert_tree(
 284        path!("/root1"),
 285        json!({
 286            "dir_1": {
 287                "nested_dir_1": {
 288                    "nested_dir_2": {
 289                        "nested_dir_3": {
 290                            "file_a.java": "// File contents",
 291                            "file_b.java": "// File contents",
 292                            "file_c.java": "// File contents",
 293                            "nested_dir_4": {
 294                                "nested_dir_5": {
 295                                    "file_d.java": "// File contents",
 296                                }
 297                            }
 298                        }
 299                    }
 300                }
 301            }
 302        }),
 303    )
 304    .await;
 305    fs.insert_tree(
 306        path!("/root2"),
 307        json!({
 308            "dir_2": {
 309                "file_1.java": "// File contents",
 310            }
 311        }),
 312    )
 313    .await;
 314
 315    // Test 1: Multiple worktrees with auto_fold_dirs = true
 316    let project = Project::test(
 317        fs.clone(),
 318        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 319        cx,
 320    )
 321    .await;
 322    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 323    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 324    cx.update(|_, cx| {
 325        let settings = *ProjectPanelSettings::get_global(cx);
 326        ProjectPanelSettings::override_global(
 327            ProjectPanelSettings {
 328                auto_fold_dirs: true,
 329                ..settings
 330            },
 331            cx,
 332        );
 333    });
 334    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 335    cx.run_until_parked();
 336    assert_eq!(
 337        visible_entries_as_strings(&panel, 0..10, cx),
 338        &[
 339            "v root1",
 340            "    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 341            "v root2",
 342            "    > dir_2",
 343        ]
 344    );
 345
 346    toggle_expand_dir(
 347        &panel,
 348        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 349        cx,
 350    );
 351    assert_eq!(
 352        visible_entries_as_strings(&panel, 0..10, cx),
 353        &[
 354            "v root1",
 355            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
 356            "        > nested_dir_4/nested_dir_5",
 357            "          file_a.java",
 358            "          file_b.java",
 359            "          file_c.java",
 360            "v root2",
 361            "    > dir_2",
 362        ]
 363    );
 364
 365    toggle_expand_dir(
 366        &panel,
 367        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
 368        cx,
 369    );
 370    assert_eq!(
 371        visible_entries_as_strings(&panel, 0..10, cx),
 372        &[
 373            "v root1",
 374            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 375            "        v nested_dir_4/nested_dir_5  <== selected",
 376            "              file_d.java",
 377            "          file_a.java",
 378            "          file_b.java",
 379            "          file_c.java",
 380            "v root2",
 381            "    > dir_2",
 382        ]
 383    );
 384    toggle_expand_dir(&panel, "root2/dir_2", cx);
 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",
 391            "              file_d.java",
 392            "          file_a.java",
 393            "          file_b.java",
 394            "          file_c.java",
 395            "v root2",
 396            "    v dir_2  <== selected",
 397            "          file_1.java",
 398        ]
 399    );
 400
 401    // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
 402    {
 403        let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
 404        let workspace =
 405            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 406        let cx = &mut VisualTestContext::from_window(*workspace, cx);
 407        cx.update(|_, cx| {
 408            let settings = *ProjectPanelSettings::get_global(cx);
 409            ProjectPanelSettings::override_global(
 410                ProjectPanelSettings {
 411                    auto_fold_dirs: true,
 412                    hide_root: true,
 413                    ..settings
 414                },
 415                cx,
 416            );
 417        });
 418        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 419        cx.run_until_parked();
 420        assert_eq!(
 421            visible_entries_as_strings(&panel, 0..10, cx),
 422            &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"],
 423            "Single worktree with hide_root=true should hide root and show auto-folded paths"
 424        );
 425
 426        toggle_expand_dir(
 427            &panel,
 428            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 429            cx,
 430        );
 431        assert_eq!(
 432            visible_entries_as_strings(&panel, 0..10, cx),
 433            &[
 434                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
 435                "    > nested_dir_4/nested_dir_5",
 436                "      file_a.java",
 437                "      file_b.java",
 438                "      file_c.java",
 439            ],
 440            "Expanded auto-folded path with hidden root should show contents without root prefix"
 441        );
 442
 443        toggle_expand_dir(
 444            &panel,
 445            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
 446            cx,
 447        );
 448        assert_eq!(
 449            visible_entries_as_strings(&panel, 0..10, cx),
 450            &[
 451                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 452                "    v nested_dir_4/nested_dir_5  <== selected",
 453                "          file_d.java",
 454                "      file_a.java",
 455                "      file_b.java",
 456                "      file_c.java",
 457            ],
 458            "Nested expansion with hidden root should maintain proper indentation"
 459        );
 460    }
 461}
 462
 463#[gpui::test(iterations = 30)]
 464async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 465    init_test(cx);
 466
 467    let fs = FakeFs::new(cx.executor());
 468    fs.insert_tree(
 469        "/root1",
 470        json!({
 471            ".dockerignore": "",
 472            ".git": {
 473                "HEAD": "",
 474            },
 475            "a": {
 476                "0": { "q": "", "r": "", "s": "" },
 477                "1": { "t": "", "u": "" },
 478                "2": { "v": "", "w": "", "x": "", "y": "" },
 479            },
 480            "b": {
 481                "3": { "Q": "" },
 482                "4": { "R": "", "S": "", "T": "", "U": "" },
 483            },
 484            "C": {
 485                "5": {},
 486                "6": { "V": "", "W": "" },
 487                "7": { "X": "" },
 488                "8": { "Y": {}, "Z": "" }
 489            }
 490        }),
 491    )
 492    .await;
 493    fs.insert_tree(
 494        "/root2",
 495        json!({
 496            "d": {
 497                "9": ""
 498            },
 499            "e": {}
 500        }),
 501    )
 502    .await;
 503
 504    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 505    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 506    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 507    let panel = workspace
 508        .update(cx, |workspace, window, cx| {
 509            let panel = ProjectPanel::new(workspace, window, cx);
 510            workspace.add_panel(panel.clone(), window, cx);
 511            panel
 512        })
 513        .unwrap();
 514    cx.run_until_parked();
 515
 516    select_path(&panel, "root1", cx);
 517    assert_eq!(
 518        visible_entries_as_strings(&panel, 0..10, cx),
 519        &[
 520            "v root1  <== selected",
 521            "    > .git",
 522            "    > a",
 523            "    > b",
 524            "    > C",
 525            "      .dockerignore",
 526            "v root2",
 527            "    > d",
 528            "    > e",
 529        ]
 530    );
 531
 532    // Add a file with the root folder selected. The filename editor is placed
 533    // before the first file in the root folder.
 534    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 535    cx.run_until_parked();
 536    panel.update_in(cx, |panel, window, cx| {
 537        assert!(panel.filename_editor.read(cx).is_focused(window));
 538    });
 539    assert_eq!(
 540        visible_entries_as_strings(&panel, 0..10, cx),
 541        &[
 542            "v root1",
 543            "    > .git",
 544            "    > a",
 545            "    > b",
 546            "    > C",
 547            "      [EDITOR: '']  <== selected",
 548            "      .dockerignore",
 549            "v root2",
 550            "    > d",
 551            "    > e",
 552        ]
 553    );
 554
 555    let confirm = panel.update_in(cx, |panel, window, cx| {
 556        panel.filename_editor.update(cx, |editor, cx| {
 557            editor.set_text("the-new-filename", window, cx)
 558        });
 559        panel.confirm_edit(true, window, cx).unwrap()
 560    });
 561    assert_eq!(
 562        visible_entries_as_strings(&panel, 0..10, cx),
 563        &[
 564            "v root1",
 565            "    > .git",
 566            "    > a",
 567            "    > b",
 568            "    > C",
 569            "      [PROCESSING: 'the-new-filename']  <== selected",
 570            "      .dockerignore",
 571            "v root2",
 572            "    > d",
 573            "    > e",
 574        ]
 575    );
 576
 577    confirm.await.unwrap();
 578    cx.run_until_parked();
 579    assert_eq!(
 580        visible_entries_as_strings(&panel, 0..10, cx),
 581        &[
 582            "v root1",
 583            "    > .git",
 584            "    > a",
 585            "    > b",
 586            "    > C",
 587            "      .dockerignore",
 588            "      the-new-filename  <== selected  <== marked",
 589            "v root2",
 590            "    > d",
 591            "    > e",
 592        ]
 593    );
 594
 595    select_path(&panel, "root1/b", cx);
 596    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 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            "    v b",
 605            "        > 3",
 606            "        > 4",
 607            "          [EDITOR: '']  <== selected",
 608            "    > C",
 609            "      .dockerignore",
 610            "      the-new-filename",
 611        ]
 612    );
 613
 614    panel
 615        .update_in(cx, |panel, window, cx| {
 616            panel.filename_editor.update(cx, |editor, cx| {
 617                editor.set_text("another-filename.txt", window, cx)
 618            });
 619            panel.confirm_edit(true, window, cx).unwrap()
 620        })
 621        .await
 622        .unwrap();
 623    cx.run_until_parked();
 624    assert_eq!(
 625        visible_entries_as_strings(&panel, 0..10, cx),
 626        &[
 627            "v root1",
 628            "    > .git",
 629            "    > a",
 630            "    v b",
 631            "        > 3",
 632            "        > 4",
 633            "          another-filename.txt  <== selected  <== marked",
 634            "    > C",
 635            "      .dockerignore",
 636            "      the-new-filename",
 637        ]
 638    );
 639
 640    select_path(&panel, "root1/b/another-filename.txt", cx);
 641    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 642    assert_eq!(
 643        visible_entries_as_strings(&panel, 0..10, cx),
 644        &[
 645            "v root1",
 646            "    > .git",
 647            "    > a",
 648            "    v b",
 649            "        > 3",
 650            "        > 4",
 651            "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
 652            "    > C",
 653            "      .dockerignore",
 654            "      the-new-filename",
 655        ]
 656    );
 657
 658    let confirm = panel.update_in(cx, |panel, window, cx| {
 659        panel.filename_editor.update(cx, |editor, cx| {
 660            let file_name_selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
 661            assert_eq!(
 662                file_name_selections.len(),
 663                1,
 664                "File editing should have a single selection, but got: {file_name_selections:?}"
 665            );
 666            let file_name_selection = &file_name_selections[0];
 667            assert_eq!(
 668                file_name_selection.start, 0,
 669                "Should select the file name from the start"
 670            );
 671            assert_eq!(
 672                file_name_selection.end,
 673                "another-filename".len(),
 674                "Should not select file extension"
 675            );
 676
 677            editor.set_text("a-different-filename.tar.gz", window, cx)
 678        });
 679        panel.confirm_edit(true, window, cx).unwrap()
 680    });
 681    assert_eq!(
 682        visible_entries_as_strings(&panel, 0..10, cx),
 683        &[
 684            "v root1",
 685            "    > .git",
 686            "    > a",
 687            "    v b",
 688            "        > 3",
 689            "        > 4",
 690            "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
 691            "    > C",
 692            "      .dockerignore",
 693            "      the-new-filename",
 694        ]
 695    );
 696
 697    confirm.await.unwrap();
 698    cx.run_until_parked();
 699    assert_eq!(
 700        visible_entries_as_strings(&panel, 0..10, cx),
 701        &[
 702            "v root1",
 703            "    > .git",
 704            "    > a",
 705            "    v b",
 706            "        > 3",
 707            "        > 4",
 708            "          a-different-filename.tar.gz  <== selected",
 709            "    > C",
 710            "      .dockerignore",
 711            "      the-new-filename",
 712        ]
 713    );
 714
 715    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 716    assert_eq!(
 717        visible_entries_as_strings(&panel, 0..10, cx),
 718        &[
 719            "v root1",
 720            "    > .git",
 721            "    > a",
 722            "    v b",
 723            "        > 3",
 724            "        > 4",
 725            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
 726            "    > C",
 727            "      .dockerignore",
 728            "      the-new-filename",
 729        ]
 730    );
 731
 732    panel.update_in(cx, |panel, window, cx| {
 733            panel.filename_editor.update(cx, |editor, cx| {
 734                let file_name_selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
 735                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
 736                let file_name_selection = &file_name_selections[0];
 737                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
 738                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
 739
 740            });
 741            panel.cancel(&menu::Cancel, window, cx)
 742        });
 743    cx.run_until_parked();
 744    panel.update_in(cx, |panel, window, cx| {
 745        panel.new_directory(&NewDirectory, window, cx)
 746    });
 747    cx.run_until_parked();
 748    assert_eq!(
 749        visible_entries_as_strings(&panel, 0..10, cx),
 750        &[
 751            "v root1",
 752            "    > .git",
 753            "    > a",
 754            "    v b",
 755            "        > [EDITOR: '']  <== selected",
 756            "        > 3",
 757            "        > 4",
 758            "          a-different-filename.tar.gz",
 759            "    > C",
 760            "      .dockerignore",
 761        ]
 762    );
 763
 764    let confirm = panel.update_in(cx, |panel, window, cx| {
 765        panel
 766            .filename_editor
 767            .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
 768        panel.confirm_edit(true, window, cx).unwrap()
 769    });
 770    panel.update_in(cx, |panel, window, cx| {
 771        panel.select_next(&Default::default(), window, cx)
 772    });
 773    assert_eq!(
 774        visible_entries_as_strings(&panel, 0..10, cx),
 775        &[
 776            "v root1",
 777            "    > .git",
 778            "    > a",
 779            "    v b",
 780            "        > [PROCESSING: 'new-dir']",
 781            "        > 3  <== selected",
 782            "        > 4",
 783            "          a-different-filename.tar.gz",
 784            "    > C",
 785            "      .dockerignore",
 786        ]
 787    );
 788
 789    confirm.await.unwrap();
 790    cx.run_until_parked();
 791    assert_eq!(
 792        visible_entries_as_strings(&panel, 0..10, cx),
 793        &[
 794            "v root1",
 795            "    > .git",
 796            "    > a",
 797            "    v b",
 798            "        > 3  <== selected",
 799            "        > 4",
 800            "        > new-dir",
 801            "          a-different-filename.tar.gz",
 802            "    > C",
 803            "      .dockerignore",
 804        ]
 805    );
 806
 807    panel.update_in(cx, |panel, window, cx| {
 808        panel.rename(&Default::default(), window, cx)
 809    });
 810    assert_eq!(
 811        visible_entries_as_strings(&panel, 0..10, cx),
 812        &[
 813            "v root1",
 814            "    > .git",
 815            "    > a",
 816            "    v b",
 817            "        > [EDITOR: '3']  <== selected",
 818            "        > 4",
 819            "        > new-dir",
 820            "          a-different-filename.tar.gz",
 821            "    > C",
 822            "      .dockerignore",
 823        ]
 824    );
 825
 826    // Dismiss the rename editor when it loses focus.
 827    workspace.update(cx, |_, window, _| window.blur()).unwrap();
 828    assert_eq!(
 829        visible_entries_as_strings(&panel, 0..10, cx),
 830        &[
 831            "v root1",
 832            "    > .git",
 833            "    > a",
 834            "    v b",
 835            "        > 3  <== selected",
 836            "        > 4",
 837            "        > new-dir",
 838            "          a-different-filename.tar.gz",
 839            "    > C",
 840            "      .dockerignore",
 841        ]
 842    );
 843
 844    // Test empty filename and filename with only whitespace
 845    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 846    cx.run_until_parked();
 847    assert_eq!(
 848        visible_entries_as_strings(&panel, 0..10, cx),
 849        &[
 850            "v root1",
 851            "    > .git",
 852            "    > a",
 853            "    v b",
 854            "        v 3",
 855            "              [EDITOR: '']  <== selected",
 856            "              Q",
 857            "        > 4",
 858            "        > new-dir",
 859            "          a-different-filename.tar.gz",
 860        ]
 861    );
 862    panel.update_in(cx, |panel, window, cx| {
 863        panel.filename_editor.update(cx, |editor, cx| {
 864            editor.set_text("", window, cx);
 865        });
 866        assert!(panel.confirm_edit(true, window, cx).is_none());
 867        panel.filename_editor.update(cx, |editor, cx| {
 868            editor.set_text("   ", window, cx);
 869        });
 870        assert!(panel.confirm_edit(true, window, cx).is_none());
 871        panel.cancel(&menu::Cancel, window, cx);
 872        panel.update_visible_entries(None, false, false, window, cx);
 873    });
 874    cx.run_until_parked();
 875    assert_eq!(
 876        visible_entries_as_strings(&panel, 0..10, cx),
 877        &[
 878            "v root1",
 879            "    > .git",
 880            "    > a",
 881            "    v b",
 882            "        v 3  <== selected",
 883            "              Q",
 884            "        > 4",
 885            "        > new-dir",
 886            "          a-different-filename.tar.gz",
 887            "    > C",
 888        ]
 889    );
 890}
 891
 892#[gpui::test(iterations = 10)]
 893async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
 894    init_test(cx);
 895
 896    let fs = FakeFs::new(cx.executor());
 897    fs.insert_tree(
 898        "/root1",
 899        json!({
 900            ".dockerignore": "",
 901            ".git": {
 902                "HEAD": "",
 903            },
 904            "a": {
 905                "0": { "q": "", "r": "", "s": "" },
 906                "1": { "t": "", "u": "" },
 907                "2": { "v": "", "w": "", "x": "", "y": "" },
 908            },
 909            "b": {
 910                "3": { "Q": "" },
 911                "4": { "R": "", "S": "", "T": "", "U": "" },
 912            },
 913            "C": {
 914                "5": {},
 915                "6": { "V": "", "W": "" },
 916                "7": { "X": "" },
 917                "8": { "Y": {}, "Z": "" }
 918            }
 919        }),
 920    )
 921    .await;
 922    fs.insert_tree(
 923        "/root2",
 924        json!({
 925            "d": {
 926                "9": ""
 927            },
 928            "e": {}
 929        }),
 930    )
 931    .await;
 932
 933    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 934    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 935    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 936    let panel = workspace
 937        .update(cx, |workspace, window, cx| {
 938            let panel = ProjectPanel::new(workspace, window, cx);
 939            workspace.add_panel(panel.clone(), window, cx);
 940            panel
 941        })
 942        .unwrap();
 943    cx.run_until_parked();
 944
 945    select_path(&panel, "root1", cx);
 946    assert_eq!(
 947        visible_entries_as_strings(&panel, 0..10, cx),
 948        &[
 949            "v root1  <== selected",
 950            "    > .git",
 951            "    > a",
 952            "    > b",
 953            "    > C",
 954            "      .dockerignore",
 955            "v root2",
 956            "    > d",
 957            "    > e",
 958        ]
 959    );
 960
 961    // Add a file with the root folder selected. The filename editor is placed
 962    // before the first file in the root folder.
 963    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 964    cx.run_until_parked();
 965    panel.update_in(cx, |panel, window, cx| {
 966        assert!(panel.filename_editor.read(cx).is_focused(window));
 967    });
 968    cx.run_until_parked();
 969    assert_eq!(
 970        visible_entries_as_strings(&panel, 0..10, cx),
 971        &[
 972            "v root1",
 973            "    > .git",
 974            "    > a",
 975            "    > b",
 976            "    > C",
 977            "      [EDITOR: '']  <== selected",
 978            "      .dockerignore",
 979            "v root2",
 980            "    > d",
 981            "    > e",
 982        ]
 983    );
 984
 985    let confirm = panel.update_in(cx, |panel, window, cx| {
 986        panel.filename_editor.update(cx, |editor, cx| {
 987            editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
 988        });
 989        panel.confirm_edit(true, window, cx).unwrap()
 990    });
 991
 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            "      [PROCESSING: 'bdir1/dir2/the-new-filename']  <== selected",
1001            "      .dockerignore",
1002            "v root2",
1003            "    > d",
1004            "    > e",
1005        ]
1006    );
1007
1008    confirm.await.unwrap();
1009    cx.run_until_parked();
1010    assert_eq!(
1011        visible_entries_as_strings(&panel, 0..13, cx),
1012        &[
1013            "v root1",
1014            "    > .git",
1015            "    > a",
1016            "    > b",
1017            "    v bdir1",
1018            "        v dir2",
1019            "              the-new-filename  <== selected  <== marked",
1020            "    > C",
1021            "      .dockerignore",
1022            "v root2",
1023            "    > d",
1024            "    > e",
1025        ]
1026    );
1027}
1028
1029#[gpui::test]
1030async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
1031    init_test(cx);
1032
1033    let fs = FakeFs::new(cx.executor());
1034    fs.insert_tree(
1035        path!("/root1"),
1036        json!({
1037            ".dockerignore": "",
1038            ".git": {
1039                "HEAD": "",
1040            },
1041        }),
1042    )
1043    .await;
1044
1045    let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
1046    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1047    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1048    let panel = workspace
1049        .update(cx, |workspace, window, cx| {
1050            let panel = ProjectPanel::new(workspace, window, cx);
1051            workspace.add_panel(panel.clone(), window, cx);
1052            panel
1053        })
1054        .unwrap();
1055    cx.run_until_parked();
1056
1057    select_path(&panel, "root1", cx);
1058    assert_eq!(
1059        visible_entries_as_strings(&panel, 0..10, cx),
1060        &["v root1  <== selected", "    > .git", "      .dockerignore",]
1061    );
1062
1063    // Add a file with the root folder selected. The filename editor is placed
1064    // before the first file in the root folder.
1065    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1066    cx.run_until_parked();
1067    panel.update_in(cx, |panel, window, cx| {
1068        assert!(panel.filename_editor.read(cx).is_focused(window));
1069    });
1070    assert_eq!(
1071        visible_entries_as_strings(&panel, 0..10, cx),
1072        &[
1073            "v root1",
1074            "    > .git",
1075            "      [EDITOR: '']  <== selected",
1076            "      .dockerignore",
1077        ]
1078    );
1079
1080    let confirm = panel.update_in(cx, |panel, window, cx| {
1081        // If we want to create a subdirectory, there should be no prefix slash.
1082        panel
1083            .filename_editor
1084            .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
1085        panel.confirm_edit(true, window, cx).unwrap()
1086    });
1087
1088    assert_eq!(
1089        visible_entries_as_strings(&panel, 0..10, cx),
1090        &[
1091            "v root1",
1092            "    > .git",
1093            "      [PROCESSING: 'new_dir']  <== selected",
1094            "      .dockerignore",
1095        ]
1096    );
1097
1098    confirm.await.unwrap();
1099    cx.run_until_parked();
1100    assert_eq!(
1101        visible_entries_as_strings(&panel, 0..10, cx),
1102        &[
1103            "v root1",
1104            "    > .git",
1105            "    v new_dir  <== selected",
1106            "      .dockerignore",
1107        ]
1108    );
1109
1110    // Test filename with whitespace
1111    select_path(&panel, "root1", cx);
1112    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1113    let confirm = panel.update_in(cx, |panel, window, cx| {
1114        // If we want to create a subdirectory, there should be no prefix slash.
1115        panel
1116            .filename_editor
1117            .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
1118        panel.confirm_edit(true, window, cx).unwrap()
1119    });
1120    confirm.await.unwrap();
1121    cx.run_until_parked();
1122    assert_eq!(
1123        visible_entries_as_strings(&panel, 0..10, cx),
1124        &[
1125            "v root1",
1126            "    > .git",
1127            "    v new dir 2  <== selected",
1128            "    v new_dir",
1129            "      .dockerignore",
1130        ]
1131    );
1132
1133    // Test filename ends with "\"
1134    #[cfg(target_os = "windows")]
1135    {
1136        select_path(&panel, "root1", cx);
1137        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1138        let confirm = panel.update_in(cx, |panel, window, cx| {
1139            // If we want to create a subdirectory, there should be no prefix slash.
1140            panel
1141                .filename_editor
1142                .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
1143            panel.confirm_edit(true, window, cx).unwrap()
1144        });
1145        confirm.await.unwrap();
1146        cx.run_until_parked();
1147        assert_eq!(
1148            visible_entries_as_strings(&panel, 0..10, cx),
1149            &[
1150                "v root1",
1151                "    > .git",
1152                "    v new dir 2",
1153                "    v new_dir",
1154                "    v new_dir_3  <== selected",
1155                "      .dockerignore",
1156            ]
1157        );
1158    }
1159}
1160
1161#[gpui::test]
1162async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1163    init_test(cx);
1164
1165    let fs = FakeFs::new(cx.executor());
1166    fs.insert_tree(
1167        "/root1",
1168        json!({
1169            "one.two.txt": "",
1170            "one.txt": ""
1171        }),
1172    )
1173    .await;
1174
1175    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1176    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1177    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1178    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1179    cx.run_until_parked();
1180
1181    panel.update_in(cx, |panel, window, cx| {
1182        panel.select_next(&Default::default(), window, cx);
1183        panel.select_next(&Default::default(), window, cx);
1184    });
1185
1186    assert_eq!(
1187        visible_entries_as_strings(&panel, 0..50, cx),
1188        &[
1189            //
1190            "v root1",
1191            "      one.txt  <== selected",
1192            "      one.two.txt",
1193        ]
1194    );
1195
1196    // Regression test - file name is created correctly when
1197    // the copied file's name contains multiple dots.
1198    panel.update_in(cx, |panel, window, cx| {
1199        panel.copy(&Default::default(), window, cx);
1200        panel.paste(&Default::default(), window, cx);
1201    });
1202    cx.executor().run_until_parked();
1203
1204    assert_eq!(
1205        visible_entries_as_strings(&panel, 0..50, cx),
1206        &[
1207            //
1208            "v root1",
1209            "      one.txt",
1210            "      [EDITOR: 'one copy.txt']  <== selected  <== marked",
1211            "      one.two.txt",
1212        ]
1213    );
1214
1215    panel.update_in(cx, |panel, window, cx| {
1216        panel.filename_editor.update(cx, |editor, cx| {
1217            let file_name_selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
1218            assert_eq!(
1219                file_name_selections.len(),
1220                1,
1221                "File editing should have a single selection, but got: {file_name_selections:?}"
1222            );
1223            let file_name_selection = &file_name_selections[0];
1224            assert_eq!(
1225                file_name_selection.start,
1226                "one".len(),
1227                "Should select the file name disambiguation after the original file name"
1228            );
1229            assert_eq!(
1230                file_name_selection.end,
1231                "one copy".len(),
1232                "Should select the file name disambiguation until the extension"
1233            );
1234        });
1235        assert!(panel.confirm_edit(true, window, cx).is_none());
1236    });
1237
1238    panel.update_in(cx, |panel, window, cx| {
1239        panel.paste(&Default::default(), window, cx);
1240    });
1241    cx.executor().run_until_parked();
1242
1243    assert_eq!(
1244        visible_entries_as_strings(&panel, 0..50, cx),
1245        &[
1246            //
1247            "v root1",
1248            "      one.txt",
1249            "      one copy.txt",
1250            "      [EDITOR: 'one copy 1.txt']  <== selected  <== marked",
1251            "      one.two.txt",
1252        ]
1253    );
1254
1255    panel.update_in(cx, |panel, window, cx| {
1256        assert!(panel.confirm_edit(true, window, cx).is_none())
1257    });
1258}
1259
1260#[gpui::test]
1261async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
1262    init_test(cx);
1263
1264    let fs = FakeFs::new(cx.executor());
1265    fs.insert_tree(
1266        "/root",
1267        json!({
1268            "one.txt": "",
1269            "two.txt": "",
1270            "a": {},
1271            "b": {}
1272        }),
1273    )
1274    .await;
1275
1276    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1277    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1278    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1279    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1280    cx.run_until_parked();
1281
1282    select_path_with_mark(&panel, "root/one.txt", cx);
1283    select_path_with_mark(&panel, "root/two.txt", cx);
1284
1285    assert_eq!(
1286        visible_entries_as_strings(&panel, 0..50, cx),
1287        &[
1288            "v root",
1289            "    > a",
1290            "    > b",
1291            "      one.txt  <== marked",
1292            "      two.txt  <== selected  <== marked",
1293        ]
1294    );
1295
1296    panel.update_in(cx, |panel, window, cx| {
1297        panel.cut(&Default::default(), window, cx);
1298    });
1299
1300    select_path(&panel, "root/a", cx);
1301
1302    panel.update_in(cx, |panel, window, cx| {
1303        panel.paste(&Default::default(), window, cx);
1304        panel.update_visible_entries(None, false, false, window, cx);
1305    });
1306    cx.executor().run_until_parked();
1307
1308    assert_eq!(
1309        visible_entries_as_strings(&panel, 0..50, cx),
1310        &[
1311            "v root",
1312            "    v a",
1313            "          one.txt  <== marked",
1314            "          two.txt  <== selected  <== marked",
1315            "    > b",
1316        ],
1317        "Cut entries should be moved on first paste."
1318    );
1319
1320    panel.update_in(cx, |panel, window, cx| {
1321        panel.cancel(&menu::Cancel {}, window, cx)
1322    });
1323    cx.executor().run_until_parked();
1324
1325    select_path(&panel, "root/b", cx);
1326
1327    panel.update_in(cx, |panel, window, cx| {
1328        panel.paste(&Default::default(), window, cx);
1329    });
1330    cx.executor().run_until_parked();
1331
1332    assert_eq!(
1333        visible_entries_as_strings(&panel, 0..50, cx),
1334        &[
1335            "v root",
1336            "    v a",
1337            "          one.txt",
1338            "          two.txt",
1339            "    v b",
1340            "          one.txt",
1341            "          two.txt  <== selected",
1342        ],
1343        "Cut entries should only be copied for the second paste!"
1344    );
1345}
1346
1347#[gpui::test]
1348async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1349    init_test(cx);
1350
1351    let fs = FakeFs::new(cx.executor());
1352    fs.insert_tree(
1353        "/root1",
1354        json!({
1355            "one.txt": "",
1356            "two.txt": "",
1357            "three.txt": "",
1358            "a": {
1359                "0": { "q": "", "r": "", "s": "" },
1360                "1": { "t": "", "u": "" },
1361                "2": { "v": "", "w": "", "x": "", "y": "" },
1362            },
1363        }),
1364    )
1365    .await;
1366
1367    fs.insert_tree(
1368        "/root2",
1369        json!({
1370            "one.txt": "",
1371            "two.txt": "",
1372            "four.txt": "",
1373            "b": {
1374                "3": { "Q": "" },
1375                "4": { "R": "", "S": "", "T": "", "U": "" },
1376            },
1377        }),
1378    )
1379    .await;
1380
1381    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1382    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1383    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1384    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1385    cx.run_until_parked();
1386
1387    select_path(&panel, "root1/three.txt", cx);
1388    panel.update_in(cx, |panel, window, cx| {
1389        panel.cut(&Default::default(), window, cx);
1390    });
1391
1392    select_path(&panel, "root2/one.txt", cx);
1393    panel.update_in(cx, |panel, window, cx| {
1394        panel.select_next(&Default::default(), window, cx);
1395        panel.paste(&Default::default(), window, cx);
1396    });
1397    cx.executor().run_until_parked();
1398    assert_eq!(
1399        visible_entries_as_strings(&panel, 0..50, cx),
1400        &[
1401            //
1402            "v root1",
1403            "    > a",
1404            "      one.txt",
1405            "      two.txt",
1406            "v root2",
1407            "    > b",
1408            "      four.txt",
1409            "      one.txt",
1410            "      three.txt  <== selected  <== marked",
1411            "      two.txt",
1412        ]
1413    );
1414
1415    select_path(&panel, "root1/a", cx);
1416    panel.update_in(cx, |panel, window, cx| {
1417        panel.cut(&Default::default(), window, cx);
1418    });
1419    select_path(&panel, "root2/two.txt", cx);
1420    panel.update_in(cx, |panel, window, cx| {
1421        panel.select_next(&Default::default(), window, cx);
1422        panel.paste(&Default::default(), window, cx);
1423    });
1424
1425    cx.executor().run_until_parked();
1426    assert_eq!(
1427        visible_entries_as_strings(&panel, 0..50, cx),
1428        &[
1429            //
1430            "v root1",
1431            "      one.txt",
1432            "      two.txt",
1433            "v root2",
1434            "    > a  <== selected",
1435            "    > b",
1436            "      four.txt",
1437            "      one.txt",
1438            "      three.txt  <== marked",
1439            "      two.txt",
1440        ]
1441    );
1442}
1443
1444#[gpui::test]
1445async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1446    init_test(cx);
1447
1448    let fs = FakeFs::new(cx.executor());
1449    fs.insert_tree(
1450        "/root1",
1451        json!({
1452            "one.txt": "",
1453            "two.txt": "",
1454            "three.txt": "",
1455            "a": {
1456                "0": { "q": "", "r": "", "s": "" },
1457                "1": { "t": "", "u": "" },
1458                "2": { "v": "", "w": "", "x": "", "y": "" },
1459            },
1460        }),
1461    )
1462    .await;
1463
1464    fs.insert_tree(
1465        "/root2",
1466        json!({
1467            "one.txt": "",
1468            "two.txt": "",
1469            "four.txt": "",
1470            "b": {
1471                "3": { "Q": "" },
1472                "4": { "R": "", "S": "", "T": "", "U": "" },
1473            },
1474        }),
1475    )
1476    .await;
1477
1478    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1479    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1480    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1481    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1482    cx.run_until_parked();
1483
1484    select_path(&panel, "root1/three.txt", cx);
1485    panel.update_in(cx, |panel, window, cx| {
1486        panel.copy(&Default::default(), window, cx);
1487    });
1488
1489    select_path(&panel, "root2/one.txt", cx);
1490    panel.update_in(cx, |panel, window, cx| {
1491        panel.select_next(&Default::default(), window, cx);
1492        panel.paste(&Default::default(), window, cx);
1493    });
1494    cx.executor().run_until_parked();
1495    assert_eq!(
1496        visible_entries_as_strings(&panel, 0..50, cx),
1497        &[
1498            //
1499            "v root1",
1500            "    > a",
1501            "      one.txt",
1502            "      three.txt",
1503            "      two.txt",
1504            "v root2",
1505            "    > b",
1506            "      four.txt",
1507            "      one.txt",
1508            "      three.txt  <== selected  <== marked",
1509            "      two.txt",
1510        ]
1511    );
1512
1513    select_path(&panel, "root1/three.txt", cx);
1514    panel.update_in(cx, |panel, window, cx| {
1515        panel.copy(&Default::default(), window, cx);
1516    });
1517    select_path(&panel, "root2/two.txt", cx);
1518    panel.update_in(cx, |panel, window, cx| {
1519        panel.select_next(&Default::default(), window, cx);
1520        panel.paste(&Default::default(), window, cx);
1521    });
1522
1523    cx.executor().run_until_parked();
1524    assert_eq!(
1525        visible_entries_as_strings(&panel, 0..50, cx),
1526        &[
1527            //
1528            "v root1",
1529            "    > a",
1530            "      one.txt",
1531            "      three.txt",
1532            "      two.txt",
1533            "v root2",
1534            "    > b",
1535            "      four.txt",
1536            "      one.txt",
1537            "      three.txt",
1538            "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
1539            "      two.txt",
1540        ]
1541    );
1542
1543    panel.update_in(cx, |panel, window, cx| {
1544        panel.cancel(&menu::Cancel {}, window, cx)
1545    });
1546    cx.executor().run_until_parked();
1547
1548    select_path(&panel, "root1/a", cx);
1549    panel.update_in(cx, |panel, window, cx| {
1550        panel.copy(&Default::default(), window, cx);
1551    });
1552    select_path(&panel, "root2/two.txt", cx);
1553    panel.update_in(cx, |panel, window, cx| {
1554        panel.select_next(&Default::default(), window, cx);
1555        panel.paste(&Default::default(), window, cx);
1556    });
1557
1558    cx.executor().run_until_parked();
1559    assert_eq!(
1560        visible_entries_as_strings(&panel, 0..50, cx),
1561        &[
1562            //
1563            "v root1",
1564            "    > a",
1565            "      one.txt",
1566            "      three.txt",
1567            "      two.txt",
1568            "v root2",
1569            "    > a  <== selected",
1570            "    > b",
1571            "      four.txt",
1572            "      one.txt",
1573            "      three.txt",
1574            "      three copy.txt",
1575            "      two.txt",
1576        ]
1577    );
1578}
1579
1580#[gpui::test]
1581async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1582    init_test(cx);
1583
1584    let fs = FakeFs::new(cx.executor());
1585    fs.insert_tree(
1586        "/root",
1587        json!({
1588            "a": {
1589                "one.txt": "",
1590                "two.txt": "",
1591                "inner_dir": {
1592                    "three.txt": "",
1593                    "four.txt": "",
1594                }
1595            },
1596            "b": {}
1597        }),
1598    )
1599    .await;
1600
1601    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1602    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1603    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1604    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1605    cx.run_until_parked();
1606
1607    select_path(&panel, "root/a", cx);
1608    panel.update_in(cx, |panel, window, cx| {
1609        panel.copy(&Default::default(), window, cx);
1610        panel.select_next(&Default::default(), window, cx);
1611        panel.paste(&Default::default(), window, cx);
1612    });
1613    cx.executor().run_until_parked();
1614
1615    let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1616    assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1617
1618    let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1619    assert_ne!(
1620        pasted_dir_file, None,
1621        "Pasted directory file should have an entry"
1622    );
1623
1624    let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1625    assert_ne!(
1626        pasted_dir_inner_dir, None,
1627        "Directories inside pasted directory should have an entry"
1628    );
1629
1630    toggle_expand_dir(&panel, "root/b/a", cx);
1631    toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1632
1633    assert_eq!(
1634        visible_entries_as_strings(&panel, 0..50, cx),
1635        &[
1636            //
1637            "v root",
1638            "    > a",
1639            "    v b",
1640            "        v a",
1641            "            v inner_dir  <== selected",
1642            "                  four.txt",
1643            "                  three.txt",
1644            "              one.txt",
1645            "              two.txt",
1646        ]
1647    );
1648
1649    select_path(&panel, "root", cx);
1650    panel.update_in(cx, |panel, window, cx| {
1651        panel.paste(&Default::default(), window, cx)
1652    });
1653    cx.executor().run_until_parked();
1654    assert_eq!(
1655        visible_entries_as_strings(&panel, 0..50, cx),
1656        &[
1657            //
1658            "v root",
1659            "    > a",
1660            "    > [EDITOR: 'a copy']  <== selected",
1661            "    v b",
1662            "        v a",
1663            "            v inner_dir",
1664            "                  four.txt",
1665            "                  three.txt",
1666            "              one.txt",
1667            "              two.txt"
1668        ]
1669    );
1670
1671    let confirm = panel.update_in(cx, |panel, window, cx| {
1672        panel
1673            .filename_editor
1674            .update(cx, |editor, cx| editor.set_text("c", window, cx));
1675        panel.confirm_edit(true, window, cx).unwrap()
1676    });
1677    assert_eq!(
1678        visible_entries_as_strings(&panel, 0..50, cx),
1679        &[
1680            //
1681            "v root",
1682            "    > a",
1683            "    > [PROCESSING: 'c']  <== selected",
1684            "    v b",
1685            "        v a",
1686            "            v inner_dir",
1687            "                  four.txt",
1688            "                  three.txt",
1689            "              one.txt",
1690            "              two.txt"
1691        ]
1692    );
1693
1694    confirm.await.unwrap();
1695
1696    panel.update_in(cx, |panel, window, cx| {
1697        panel.paste(&Default::default(), window, cx)
1698    });
1699    cx.executor().run_until_parked();
1700    assert_eq!(
1701        visible_entries_as_strings(&panel, 0..50, cx),
1702        &[
1703            //
1704            "v root",
1705            "    > a",
1706            "    v b",
1707            "        v a",
1708            "            v inner_dir",
1709            "                  four.txt",
1710            "                  three.txt",
1711            "              one.txt",
1712            "              two.txt",
1713            "    v c",
1714            "        > a  <== selected",
1715            "        > inner_dir",
1716            "          one.txt",
1717            "          two.txt",
1718        ]
1719    );
1720}
1721
1722#[gpui::test]
1723async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1724    init_test(cx);
1725
1726    let fs = FakeFs::new(cx.executor());
1727    fs.insert_tree(
1728        "/test",
1729        json!({
1730            "dir1": {
1731                "a.txt": "",
1732                "b.txt": "",
1733            },
1734            "dir2": {},
1735            "c.txt": "",
1736            "d.txt": "",
1737        }),
1738    )
1739    .await;
1740
1741    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1742    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1743    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1744    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1745    cx.run_until_parked();
1746
1747    toggle_expand_dir(&panel, "test/dir1", cx);
1748
1749    cx.simulate_modifiers_change(gpui::Modifiers {
1750        control: true,
1751        ..Default::default()
1752    });
1753
1754    select_path_with_mark(&panel, "test/dir1", cx);
1755    select_path_with_mark(&panel, "test/c.txt", cx);
1756
1757    assert_eq!(
1758        visible_entries_as_strings(&panel, 0..15, cx),
1759        &[
1760            "v test",
1761            "    v dir1  <== marked",
1762            "          a.txt",
1763            "          b.txt",
1764            "    > dir2",
1765            "      c.txt  <== selected  <== marked",
1766            "      d.txt",
1767        ],
1768        "Initial state before copying dir1 and c.txt"
1769    );
1770
1771    panel.update_in(cx, |panel, window, cx| {
1772        panel.copy(&Default::default(), window, cx);
1773    });
1774    select_path(&panel, "test/dir2", cx);
1775    panel.update_in(cx, |panel, window, cx| {
1776        panel.paste(&Default::default(), window, cx);
1777    });
1778    cx.executor().run_until_parked();
1779
1780    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1781
1782    assert_eq!(
1783        visible_entries_as_strings(&panel, 0..15, cx),
1784        &[
1785            "v test",
1786            "    v dir1  <== marked",
1787            "          a.txt",
1788            "          b.txt",
1789            "    v dir2",
1790            "        v dir1  <== selected",
1791            "              a.txt",
1792            "              b.txt",
1793            "          c.txt",
1794            "      c.txt  <== marked",
1795            "      d.txt",
1796        ],
1797        "Should copy dir1 as well as c.txt into dir2"
1798    );
1799
1800    // Disambiguating multiple files should not open the rename editor.
1801    select_path(&panel, "test/dir2", cx);
1802    panel.update_in(cx, |panel, window, cx| {
1803        panel.paste(&Default::default(), window, cx);
1804    });
1805    cx.executor().run_until_parked();
1806
1807    assert_eq!(
1808        visible_entries_as_strings(&panel, 0..15, cx),
1809        &[
1810            "v test",
1811            "    v dir1  <== marked",
1812            "          a.txt",
1813            "          b.txt",
1814            "    v dir2",
1815            "        v dir1",
1816            "              a.txt",
1817            "              b.txt",
1818            "        > dir1 copy  <== selected",
1819            "          c.txt",
1820            "          c copy.txt",
1821            "      c.txt  <== marked",
1822            "      d.txt",
1823        ],
1824        "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1825    );
1826}
1827
1828#[gpui::test]
1829async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1830    init_test(cx);
1831
1832    let fs = FakeFs::new(cx.executor());
1833    fs.insert_tree(
1834        "/test",
1835        json!({
1836            "dir1": {
1837                "a.txt": "",
1838                "b.txt": "",
1839            },
1840            "dir2": {},
1841            "c.txt": "",
1842            "d.txt": "",
1843        }),
1844    )
1845    .await;
1846
1847    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1848    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1849    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1850    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1851    cx.run_until_parked();
1852
1853    toggle_expand_dir(&panel, "test/dir1", cx);
1854
1855    cx.simulate_modifiers_change(gpui::Modifiers {
1856        control: true,
1857        ..Default::default()
1858    });
1859
1860    select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1861    select_path_with_mark(&panel, "test/dir1", cx);
1862    select_path_with_mark(&panel, "test/c.txt", cx);
1863
1864    assert_eq!(
1865        visible_entries_as_strings(&panel, 0..15, cx),
1866        &[
1867            "v test",
1868            "    v dir1  <== marked",
1869            "          a.txt  <== marked",
1870            "          b.txt",
1871            "    > dir2",
1872            "      c.txt  <== selected  <== marked",
1873            "      d.txt",
1874        ],
1875        "Initial state before copying a.txt, dir1 and c.txt"
1876    );
1877
1878    panel.update_in(cx, |panel, window, cx| {
1879        panel.copy(&Default::default(), window, cx);
1880    });
1881    select_path(&panel, "test/dir2", cx);
1882    panel.update_in(cx, |panel, window, cx| {
1883        panel.paste(&Default::default(), window, cx);
1884    });
1885    cx.executor().run_until_parked();
1886
1887    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1888
1889    assert_eq!(
1890        visible_entries_as_strings(&panel, 0..20, cx),
1891        &[
1892            "v test",
1893            "    v dir1  <== marked",
1894            "          a.txt  <== marked",
1895            "          b.txt",
1896            "    v dir2",
1897            "        v dir1  <== selected",
1898            "              a.txt",
1899            "              b.txt",
1900            "          c.txt",
1901            "      c.txt  <== marked",
1902            "      d.txt",
1903        ],
1904        "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1905    );
1906}
1907
1908#[gpui::test]
1909async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1910    init_test_with_editor(cx);
1911
1912    let fs = FakeFs::new(cx.executor());
1913    fs.insert_tree(
1914        path!("/src"),
1915        json!({
1916            "test": {
1917                "first.rs": "// First Rust file",
1918                "second.rs": "// Second Rust file",
1919                "third.rs": "// Third Rust file",
1920            }
1921        }),
1922    )
1923    .await;
1924
1925    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1926    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1927    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1928    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1929    cx.run_until_parked();
1930
1931    toggle_expand_dir(&panel, "src/test", cx);
1932    select_path(&panel, "src/test/first.rs", cx);
1933    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1934    cx.executor().run_until_parked();
1935    assert_eq!(
1936        visible_entries_as_strings(&panel, 0..10, cx),
1937        &[
1938            "v src",
1939            "    v test",
1940            "          first.rs  <== selected  <== marked",
1941            "          second.rs",
1942            "          third.rs"
1943        ]
1944    );
1945    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1946
1947    submit_deletion(&panel, cx);
1948    assert_eq!(
1949        visible_entries_as_strings(&panel, 0..10, cx),
1950        &[
1951            "v src",
1952            "    v test",
1953            "          second.rs  <== selected",
1954            "          third.rs"
1955        ],
1956        "Project panel should have no deleted file, no other file is selected in it"
1957    );
1958    ensure_no_open_items_and_panes(&workspace, cx);
1959
1960    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1961    cx.executor().run_until_parked();
1962    assert_eq!(
1963        visible_entries_as_strings(&panel, 0..10, cx),
1964        &[
1965            "v src",
1966            "    v test",
1967            "          second.rs  <== selected  <== marked",
1968            "          third.rs"
1969        ]
1970    );
1971    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1972
1973    workspace
1974        .update(cx, |workspace, window, cx| {
1975            let active_items = workspace
1976                .panes()
1977                .iter()
1978                .filter_map(|pane| pane.read(cx).active_item())
1979                .collect::<Vec<_>>();
1980            assert_eq!(active_items.len(), 1);
1981            let open_editor = active_items
1982                .into_iter()
1983                .next()
1984                .unwrap()
1985                .downcast::<Editor>()
1986                .expect("Open item should be an editor");
1987            open_editor.update(cx, |editor, cx| {
1988                editor.set_text("Another text!", window, cx)
1989            });
1990        })
1991        .unwrap();
1992    submit_deletion_skipping_prompt(&panel, cx);
1993    assert_eq!(
1994        visible_entries_as_strings(&panel, 0..10, cx),
1995        &["v src", "    v test", "          third.rs  <== selected"],
1996        "Project panel should have no deleted file, with one last file remaining"
1997    );
1998    ensure_no_open_items_and_panes(&workspace, cx);
1999}
2000
2001#[gpui::test]
2002async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2003    init_test_with_editor(cx);
2004
2005    let fs = FakeFs::new(cx.executor());
2006    fs.insert_tree(
2007        "/src",
2008        json!({
2009            "test": {
2010                "first.rs": "// First Rust file",
2011                "second.rs": "// Second Rust file",
2012                "third.rs": "// Third Rust file",
2013            }
2014        }),
2015    )
2016    .await;
2017
2018    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2019    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2020    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2021    let panel = workspace
2022        .update(cx, |workspace, window, cx| {
2023            let panel = ProjectPanel::new(workspace, window, cx);
2024            workspace.add_panel(panel.clone(), window, cx);
2025            panel
2026        })
2027        .unwrap();
2028    cx.run_until_parked();
2029
2030    select_path(&panel, "src", cx);
2031    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2032    cx.executor().run_until_parked();
2033    assert_eq!(
2034        visible_entries_as_strings(&panel, 0..10, cx),
2035        &[
2036            //
2037            "v src  <== selected",
2038            "    > test"
2039        ]
2040    );
2041    panel.update_in(cx, |panel, window, cx| {
2042        panel.new_directory(&NewDirectory, window, cx)
2043    });
2044    cx.run_until_parked();
2045    panel.update_in(cx, |panel, window, cx| {
2046        assert!(panel.filename_editor.read(cx).is_focused(window));
2047    });
2048    cx.executor().run_until_parked();
2049    assert_eq!(
2050        visible_entries_as_strings(&panel, 0..10, cx),
2051        &[
2052            //
2053            "v src",
2054            "    > [EDITOR: '']  <== selected",
2055            "    > test"
2056        ]
2057    );
2058    panel.update_in(cx, |panel, window, cx| {
2059        panel
2060            .filename_editor
2061            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2062        assert!(
2063            panel.confirm_edit(true, window, cx).is_none(),
2064            "Should not allow to confirm on conflicting new directory name"
2065        );
2066    });
2067    cx.executor().run_until_parked();
2068    panel.update_in(cx, |panel, window, cx| {
2069        assert!(
2070            panel.state.edit_state.is_some(),
2071            "Edit state should not be None after conflicting new directory name"
2072        );
2073        panel.cancel(&menu::Cancel, window, cx);
2074        panel.update_visible_entries(None, false, false, window, cx);
2075    });
2076    cx.run_until_parked();
2077    assert_eq!(
2078        visible_entries_as_strings(&panel, 0..10, cx),
2079        &[
2080            //
2081            "v src  <== selected",
2082            "    > test"
2083        ],
2084        "File list should be unchanged after failed folder create confirmation"
2085    );
2086
2087    select_path(&panel, "src/test", cx);
2088    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2089    cx.executor().run_until_parked();
2090    assert_eq!(
2091        visible_entries_as_strings(&panel, 0..10, cx),
2092        &[
2093            //
2094            "v src",
2095            "    > test  <== selected"
2096        ]
2097    );
2098    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2099    cx.run_until_parked();
2100    panel.update_in(cx, |panel, window, cx| {
2101        assert!(panel.filename_editor.read(cx).is_focused(window));
2102    });
2103    assert_eq!(
2104        visible_entries_as_strings(&panel, 0..10, cx),
2105        &[
2106            "v src",
2107            "    v test",
2108            "          [EDITOR: '']  <== selected",
2109            "          first.rs",
2110            "          second.rs",
2111            "          third.rs"
2112        ]
2113    );
2114    panel.update_in(cx, |panel, window, cx| {
2115        panel
2116            .filename_editor
2117            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2118        assert!(
2119            panel.confirm_edit(true, window, cx).is_none(),
2120            "Should not allow to confirm on conflicting new file name"
2121        );
2122    });
2123    cx.executor().run_until_parked();
2124    panel.update_in(cx, |panel, window, cx| {
2125        assert!(
2126            panel.state.edit_state.is_some(),
2127            "Edit state should not be None after conflicting new file name"
2128        );
2129        panel.cancel(&menu::Cancel, window, cx);
2130        panel.update_visible_entries(None, false, false, window, cx);
2131    });
2132    cx.run_until_parked();
2133    assert_eq!(
2134        visible_entries_as_strings(&panel, 0..10, cx),
2135        &[
2136            "v src",
2137            "    v test  <== selected",
2138            "          first.rs",
2139            "          second.rs",
2140            "          third.rs"
2141        ],
2142        "File list should be unchanged after failed file create confirmation"
2143    );
2144
2145    select_path(&panel, "src/test/first.rs", cx);
2146    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2147    cx.executor().run_until_parked();
2148    assert_eq!(
2149        visible_entries_as_strings(&panel, 0..10, cx),
2150        &[
2151            "v src",
2152            "    v test",
2153            "          first.rs  <== selected",
2154            "          second.rs",
2155            "          third.rs"
2156        ],
2157    );
2158    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2159    panel.update_in(cx, |panel, window, cx| {
2160        assert!(panel.filename_editor.read(cx).is_focused(window));
2161    });
2162    assert_eq!(
2163        visible_entries_as_strings(&panel, 0..10, cx),
2164        &[
2165            "v src",
2166            "    v test",
2167            "          [EDITOR: 'first.rs']  <== selected",
2168            "          second.rs",
2169            "          third.rs"
2170        ]
2171    );
2172    panel.update_in(cx, |panel, window, cx| {
2173        panel
2174            .filename_editor
2175            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2176        assert!(
2177            panel.confirm_edit(true, window, cx).is_none(),
2178            "Should not allow to confirm on conflicting file rename"
2179        )
2180    });
2181    cx.executor().run_until_parked();
2182    panel.update_in(cx, |panel, window, cx| {
2183        assert!(
2184            panel.state.edit_state.is_some(),
2185            "Edit state should not be None after conflicting file rename"
2186        );
2187        panel.cancel(&menu::Cancel, window, cx);
2188    });
2189    assert_eq!(
2190        visible_entries_as_strings(&panel, 0..10, cx),
2191        &[
2192            "v src",
2193            "    v test",
2194            "          first.rs  <== selected",
2195            "          second.rs",
2196            "          third.rs"
2197        ],
2198        "File list should be unchanged after failed rename confirmation"
2199    );
2200}
2201
2202// NOTE: This test is skipped on Windows, because on Windows,
2203// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
2204// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
2205#[gpui::test]
2206#[cfg_attr(target_os = "windows", ignore)]
2207async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
2208    init_test_with_editor(cx);
2209
2210    let fs = FakeFs::new(cx.executor());
2211    fs.insert_tree(
2212        "/src",
2213        json!({
2214            "test": {
2215                "first.txt": "// First Txt file",
2216                "second.txt": "// Second Txt file",
2217                "third.txt": "// Third Txt file",
2218            }
2219        }),
2220    )
2221    .await;
2222
2223    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2224    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2225    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2226    let panel = workspace
2227        .update(cx, |workspace, window, cx| {
2228            let panel = ProjectPanel::new(workspace, window, cx);
2229            workspace.add_panel(panel.clone(), window, cx);
2230            panel
2231        })
2232        .unwrap();
2233    cx.run_until_parked();
2234
2235    select_path(&panel, "src", cx);
2236    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2237    cx.executor().run_until_parked();
2238    assert_eq!(
2239        visible_entries_as_strings(&panel, 0..10, cx),
2240        &[
2241            //
2242            "v src  <== selected",
2243            "    > test"
2244        ]
2245    );
2246    panel.update_in(cx, |panel, window, cx| {
2247        panel.new_directory(&NewDirectory, window, cx)
2248    });
2249    cx.run_until_parked();
2250    panel.update_in(cx, |panel, window, cx| {
2251        assert!(panel.filename_editor.read(cx).is_focused(window));
2252    });
2253    cx.executor().run_until_parked();
2254    assert_eq!(
2255        visible_entries_as_strings(&panel, 0..10, cx),
2256        &[
2257            //
2258            "v src",
2259            "    > [EDITOR: '']  <== selected",
2260            "    > test"
2261        ]
2262    );
2263    panel.update_in(cx, |panel, window, cx| {
2264        panel
2265            .filename_editor
2266            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2267        assert!(
2268            panel.confirm_edit(true, window, cx).is_none(),
2269            "Should not allow to confirm on conflicting new directory name"
2270        );
2271    });
2272    cx.executor().run_until_parked();
2273    panel.update_in(cx, |panel, window, cx| {
2274        assert!(
2275            panel.state.edit_state.is_some(),
2276            "Edit state should not be None after conflicting new directory name"
2277        );
2278        panel.cancel(&menu::Cancel, window, cx);
2279        panel.update_visible_entries(None, false, false, window, cx);
2280    });
2281    cx.run_until_parked();
2282    assert_eq!(
2283        visible_entries_as_strings(&panel, 0..10, cx),
2284        &[
2285            //
2286            "v src  <== selected",
2287            "    > test"
2288        ],
2289        "File list should be unchanged after failed folder create confirmation"
2290    );
2291
2292    select_path(&panel, "src/test", cx);
2293    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2294    cx.executor().run_until_parked();
2295    assert_eq!(
2296        visible_entries_as_strings(&panel, 0..10, cx),
2297        &[
2298            //
2299            "v src",
2300            "    > test  <== selected"
2301        ]
2302    );
2303    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2304    cx.run_until_parked();
2305    panel.update_in(cx, |panel, window, cx| {
2306        assert!(panel.filename_editor.read(cx).is_focused(window));
2307    });
2308    assert_eq!(
2309        visible_entries_as_strings(&panel, 0..10, cx),
2310        &[
2311            "v src",
2312            "    v test",
2313            "          [EDITOR: '']  <== selected",
2314            "          first.txt",
2315            "          second.txt",
2316            "          third.txt"
2317        ]
2318    );
2319    panel.update_in(cx, |panel, window, cx| {
2320        panel
2321            .filename_editor
2322            .update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
2323        assert!(
2324            panel.confirm_edit(true, window, cx).is_none(),
2325            "Should not allow to confirm on conflicting new file name"
2326        );
2327    });
2328    cx.executor().run_until_parked();
2329    panel.update_in(cx, |panel, window, cx| {
2330        assert!(
2331            panel.state.edit_state.is_some(),
2332            "Edit state should not be None after conflicting new file name"
2333        );
2334        panel.cancel(&menu::Cancel, window, cx);
2335        panel.update_visible_entries(None, false, false, window, cx);
2336    });
2337    cx.run_until_parked();
2338    assert_eq!(
2339        visible_entries_as_strings(&panel, 0..10, cx),
2340        &[
2341            "v src",
2342            "    v test  <== selected",
2343            "          first.txt",
2344            "          second.txt",
2345            "          third.txt"
2346        ],
2347        "File list should be unchanged after failed file create confirmation"
2348    );
2349
2350    select_path(&panel, "src/test/first.txt", cx);
2351    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2352    cx.executor().run_until_parked();
2353    assert_eq!(
2354        visible_entries_as_strings(&panel, 0..10, cx),
2355        &[
2356            "v src",
2357            "    v test",
2358            "          first.txt  <== selected",
2359            "          second.txt",
2360            "          third.txt"
2361        ],
2362    );
2363    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2364    panel.update_in(cx, |panel, window, cx| {
2365        assert!(panel.filename_editor.read(cx).is_focused(window));
2366    });
2367    assert_eq!(
2368        visible_entries_as_strings(&panel, 0..10, cx),
2369        &[
2370            "v src",
2371            "    v test",
2372            "          [EDITOR: 'first.txt']  <== selected",
2373            "          second.txt",
2374            "          third.txt"
2375        ]
2376    );
2377    panel.update_in(cx, |panel, window, cx| {
2378        panel
2379            .filename_editor
2380            .update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
2381        assert!(
2382            panel.confirm_edit(true, window, cx).is_none(),
2383            "Should not allow to confirm on conflicting file rename"
2384        )
2385    });
2386    cx.executor().run_until_parked();
2387    panel.update_in(cx, |panel, window, cx| {
2388        assert!(
2389            panel.state.edit_state.is_some(),
2390            "Edit state should not be None after conflicting file rename"
2391        );
2392        panel.cancel(&menu::Cancel, window, cx);
2393    });
2394    assert_eq!(
2395        visible_entries_as_strings(&panel, 0..10, cx),
2396        &[
2397            "v src",
2398            "    v test",
2399            "          first.txt  <== selected",
2400            "          second.txt",
2401            "          third.txt"
2402        ],
2403        "File list should be unchanged after failed rename confirmation"
2404    );
2405    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2406    cx.executor().run_until_parked();
2407    // Try to duplicate and check history
2408    panel.update_in(cx, |panel, window, cx| {
2409        panel.duplicate(&Duplicate, window, cx)
2410    });
2411    cx.executor().run_until_parked();
2412
2413    assert_eq!(
2414        visible_entries_as_strings(&panel, 0..10, cx),
2415        &[
2416            "v src",
2417            "    v test",
2418            "          first.txt",
2419            "          [EDITOR: 'first copy.txt']  <== selected  <== marked",
2420            "          second.txt",
2421            "          third.txt"
2422        ],
2423    );
2424
2425    let confirm = panel.update_in(cx, |panel, window, cx| {
2426        panel
2427            .filename_editor
2428            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2429        panel.confirm_edit(true, window, cx).unwrap()
2430    });
2431    confirm.await.unwrap();
2432    cx.executor().run_until_parked();
2433
2434    assert_eq!(
2435        visible_entries_as_strings(&panel, 0..10, cx),
2436        &[
2437            "v src",
2438            "    v test",
2439            "          first.txt",
2440            "          fourth.txt  <== selected",
2441            "          second.txt",
2442            "          third.txt"
2443        ],
2444        "File list should be different after rename confirmation"
2445    );
2446
2447    panel.update_in(cx, |panel, window, cx| {
2448        panel.update_visible_entries(None, false, false, window, cx);
2449    });
2450    cx.executor().run_until_parked();
2451
2452    select_path(&panel, "src/test/first.txt", cx);
2453    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2454    cx.executor().run_until_parked();
2455
2456    workspace
2457        .read_with(cx, |this, cx| {
2458            assert!(
2459                this.recent_navigation_history_iter(cx)
2460                    .any(|(project_path, abs_path)| {
2461                        project_path.path == Arc::from(rel_path("test/fourth.txt"))
2462                            && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2463                    })
2464            );
2465        })
2466        .unwrap();
2467}
2468
2469// NOTE: This test is skipped on Windows, because on Windows,
2470// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
2471// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
2472#[gpui::test]
2473#[cfg_attr(target_os = "windows", ignore)]
2474async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
2475    init_test_with_editor(cx);
2476
2477    let fs = FakeFs::new(cx.executor());
2478    fs.insert_tree(
2479        "/src",
2480        json!({
2481            "test": {
2482                "first.txt": "// First Txt file",
2483                "second.txt": "// Second Txt file",
2484                "third.txt": "// Third Txt file",
2485            }
2486        }),
2487    )
2488    .await;
2489
2490    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2491    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2492    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2493    let panel = workspace
2494        .update(cx, |workspace, window, cx| {
2495            let panel = ProjectPanel::new(workspace, window, cx);
2496            workspace.add_panel(panel.clone(), window, cx);
2497            panel
2498        })
2499        .unwrap();
2500    cx.run_until_parked();
2501
2502    select_path(&panel, "src", cx);
2503    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2504    cx.executor().run_until_parked();
2505    assert_eq!(
2506        visible_entries_as_strings(&panel, 0..10, cx),
2507        &[
2508            //
2509            "v src  <== selected",
2510            "    > test"
2511        ]
2512    );
2513
2514    select_path(&panel, "src/test", cx);
2515    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2516    cx.executor().run_until_parked();
2517    assert_eq!(
2518        visible_entries_as_strings(&panel, 0..10, cx),
2519        &[
2520            //
2521            "v src",
2522            "    > test  <== selected"
2523        ]
2524    );
2525    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2526    cx.run_until_parked();
2527    panel.update_in(cx, |panel, window, cx| {
2528        assert!(panel.filename_editor.read(cx).is_focused(window));
2529    });
2530
2531    select_path(&panel, "src/test/first.txt", cx);
2532    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2533    cx.executor().run_until_parked();
2534
2535    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2536    cx.executor().run_until_parked();
2537
2538    assert_eq!(
2539        visible_entries_as_strings(&panel, 0..10, cx),
2540        &[
2541            "v src",
2542            "    v test",
2543            "          [EDITOR: 'first.txt']  <== selected  <== marked",
2544            "          second.txt",
2545            "          third.txt"
2546        ],
2547    );
2548
2549    let confirm = panel.update_in(cx, |panel, window, cx| {
2550        panel
2551            .filename_editor
2552            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2553        panel.confirm_edit(true, window, cx).unwrap()
2554    });
2555    confirm.await.unwrap();
2556    cx.executor().run_until_parked();
2557
2558    assert_eq!(
2559        visible_entries_as_strings(&panel, 0..10, cx),
2560        &[
2561            "v src",
2562            "    v test",
2563            "          fourth.txt  <== selected",
2564            "          second.txt",
2565            "          third.txt"
2566        ],
2567        "File list should be different after rename confirmation"
2568    );
2569
2570    panel.update_in(cx, |panel, window, cx| {
2571        panel.update_visible_entries(None, false, false, window, cx);
2572    });
2573    cx.executor().run_until_parked();
2574
2575    select_path(&panel, "src/test/second.txt", cx);
2576    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2577    cx.executor().run_until_parked();
2578
2579    workspace
2580        .read_with(cx, |this, cx| {
2581            assert!(
2582                this.recent_navigation_history_iter(cx)
2583                    .any(|(project_path, abs_path)| {
2584                        project_path.path == Arc::from(rel_path("test/fourth.txt"))
2585                            && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2586                    })
2587            );
2588        })
2589        .unwrap();
2590}
2591
2592#[gpui::test]
2593async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2594    init_test_with_editor(cx);
2595
2596    let fs = FakeFs::new(cx.executor());
2597    fs.insert_tree(
2598        path!("/root"),
2599        json!({
2600            "tree1": {
2601                ".git": {},
2602                "dir1": {
2603                    "modified1.txt": "1",
2604                    "unmodified1.txt": "1",
2605                    "modified2.txt": "1",
2606                },
2607                "dir2": {
2608                    "modified3.txt": "1",
2609                    "unmodified2.txt": "1",
2610                },
2611                "modified4.txt": "1",
2612                "unmodified3.txt": "1",
2613            },
2614            "tree2": {
2615                ".git": {},
2616                "dir3": {
2617                    "modified5.txt": "1",
2618                    "unmodified4.txt": "1",
2619                },
2620                "modified6.txt": "1",
2621                "unmodified5.txt": "1",
2622            }
2623        }),
2624    )
2625    .await;
2626
2627    // Mark files as git modified
2628    fs.set_head_and_index_for_repo(
2629        path!("/root/tree1/.git").as_ref(),
2630        &[
2631            ("dir1/modified1.txt", "modified".into()),
2632            ("dir1/modified2.txt", "modified".into()),
2633            ("modified4.txt", "modified".into()),
2634            ("dir2/modified3.txt", "modified".into()),
2635        ],
2636    );
2637    fs.set_head_and_index_for_repo(
2638        path!("/root/tree2/.git").as_ref(),
2639        &[
2640            ("dir3/modified5.txt", "modified".into()),
2641            ("modified6.txt", "modified".into()),
2642        ],
2643    );
2644
2645    let project = Project::test(
2646        fs.clone(),
2647        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2648        cx,
2649    )
2650    .await;
2651
2652    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2653        let mut worktrees = project.worktrees(cx);
2654        let worktree1 = worktrees.next().unwrap();
2655        let worktree2 = worktrees.next().unwrap();
2656        (
2657            worktree1.read(cx).as_local().unwrap().scan_complete(),
2658            worktree2.read(cx).as_local().unwrap().scan_complete(),
2659        )
2660    });
2661    scan1_complete.await;
2662    scan2_complete.await;
2663    cx.run_until_parked();
2664
2665    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2666    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2667    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2668    cx.run_until_parked();
2669
2670    // Check initial state
2671    assert_eq!(
2672        visible_entries_as_strings(&panel, 0..15, cx),
2673        &[
2674            "v tree1",
2675            "    > .git",
2676            "    > dir1",
2677            "    > dir2",
2678            "      modified4.txt",
2679            "      unmodified3.txt",
2680            "v tree2",
2681            "    > .git",
2682            "    > dir3",
2683            "      modified6.txt",
2684            "      unmodified5.txt"
2685        ],
2686    );
2687
2688    // Test selecting next modified entry
2689    panel.update_in(cx, |panel, window, cx| {
2690        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2691    });
2692    cx.run_until_parked();
2693
2694    assert_eq!(
2695        visible_entries_as_strings(&panel, 0..6, cx),
2696        &[
2697            "v tree1",
2698            "    > .git",
2699            "    v dir1",
2700            "          modified1.txt  <== selected",
2701            "          modified2.txt",
2702            "          unmodified1.txt",
2703        ],
2704    );
2705
2706    panel.update_in(cx, |panel, window, cx| {
2707        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2708    });
2709    cx.run_until_parked();
2710
2711    assert_eq!(
2712        visible_entries_as_strings(&panel, 0..6, cx),
2713        &[
2714            "v tree1",
2715            "    > .git",
2716            "    v dir1",
2717            "          modified1.txt",
2718            "          modified2.txt  <== selected",
2719            "          unmodified1.txt",
2720        ],
2721    );
2722
2723    panel.update_in(cx, |panel, window, cx| {
2724        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2725    });
2726    cx.run_until_parked();
2727
2728    assert_eq!(
2729        visible_entries_as_strings(&panel, 6..9, cx),
2730        &[
2731            "    v dir2",
2732            "          modified3.txt  <== selected",
2733            "          unmodified2.txt",
2734        ],
2735    );
2736
2737    panel.update_in(cx, |panel, window, cx| {
2738        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2739    });
2740    cx.run_until_parked();
2741
2742    assert_eq!(
2743        visible_entries_as_strings(&panel, 9..11, cx),
2744        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2745    );
2746
2747    panel.update_in(cx, |panel, window, cx| {
2748        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2749    });
2750    cx.run_until_parked();
2751
2752    assert_eq!(
2753        visible_entries_as_strings(&panel, 13..16, cx),
2754        &[
2755            "    v dir3",
2756            "          modified5.txt  <== selected",
2757            "          unmodified4.txt",
2758        ],
2759    );
2760
2761    panel.update_in(cx, |panel, window, cx| {
2762        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2763    });
2764    cx.run_until_parked();
2765
2766    assert_eq!(
2767        visible_entries_as_strings(&panel, 16..18, cx),
2768        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2769    );
2770
2771    // Wraps around to first modified file
2772    panel.update_in(cx, |panel, window, cx| {
2773        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2774    });
2775    cx.run_until_parked();
2776
2777    assert_eq!(
2778        visible_entries_as_strings(&panel, 0..18, cx),
2779        &[
2780            "v tree1",
2781            "    > .git",
2782            "    v dir1",
2783            "          modified1.txt  <== selected",
2784            "          modified2.txt",
2785            "          unmodified1.txt",
2786            "    v dir2",
2787            "          modified3.txt",
2788            "          unmodified2.txt",
2789            "      modified4.txt",
2790            "      unmodified3.txt",
2791            "v tree2",
2792            "    > .git",
2793            "    v dir3",
2794            "          modified5.txt",
2795            "          unmodified4.txt",
2796            "      modified6.txt",
2797            "      unmodified5.txt",
2798        ],
2799    );
2800
2801    // Wraps around again to last modified file
2802    panel.update_in(cx, |panel, window, cx| {
2803        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2804    });
2805    cx.run_until_parked();
2806
2807    assert_eq!(
2808        visible_entries_as_strings(&panel, 16..18, cx),
2809        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2810    );
2811
2812    panel.update_in(cx, |panel, window, cx| {
2813        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2814    });
2815    cx.run_until_parked();
2816
2817    assert_eq!(
2818        visible_entries_as_strings(&panel, 13..16, cx),
2819        &[
2820            "    v dir3",
2821            "          modified5.txt  <== selected",
2822            "          unmodified4.txt",
2823        ],
2824    );
2825
2826    panel.update_in(cx, |panel, window, cx| {
2827        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2828    });
2829    cx.run_until_parked();
2830
2831    assert_eq!(
2832        visible_entries_as_strings(&panel, 9..11, cx),
2833        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2834    );
2835
2836    panel.update_in(cx, |panel, window, cx| {
2837        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2838    });
2839    cx.run_until_parked();
2840
2841    assert_eq!(
2842        visible_entries_as_strings(&panel, 6..9, cx),
2843        &[
2844            "    v dir2",
2845            "          modified3.txt  <== selected",
2846            "          unmodified2.txt",
2847        ],
2848    );
2849
2850    panel.update_in(cx, |panel, window, cx| {
2851        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2852    });
2853    cx.run_until_parked();
2854
2855    assert_eq!(
2856        visible_entries_as_strings(&panel, 0..6, cx),
2857        &[
2858            "v tree1",
2859            "    > .git",
2860            "    v dir1",
2861            "          modified1.txt",
2862            "          modified2.txt  <== selected",
2863            "          unmodified1.txt",
2864        ],
2865    );
2866
2867    panel.update_in(cx, |panel, window, cx| {
2868        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2869    });
2870    cx.run_until_parked();
2871
2872    assert_eq!(
2873        visible_entries_as_strings(&panel, 0..6, cx),
2874        &[
2875            "v tree1",
2876            "    > .git",
2877            "    v dir1",
2878            "          modified1.txt  <== selected",
2879            "          modified2.txt",
2880            "          unmodified1.txt",
2881        ],
2882    );
2883}
2884
2885#[gpui::test]
2886async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2887    init_test_with_editor(cx);
2888
2889    let fs = FakeFs::new(cx.executor());
2890    fs.insert_tree(
2891        "/project_root",
2892        json!({
2893            "dir_1": {
2894                "nested_dir": {
2895                    "file_a.py": "# File contents",
2896                }
2897            },
2898            "file_1.py": "# File contents",
2899            "dir_2": {
2900
2901            },
2902            "dir_3": {
2903
2904            },
2905            "file_2.py": "# File contents",
2906            "dir_4": {
2907
2908            },
2909        }),
2910    )
2911    .await;
2912
2913    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2914    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2915    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2916    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2917    cx.run_until_parked();
2918
2919    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2920    cx.executor().run_until_parked();
2921    select_path(&panel, "project_root/dir_1", cx);
2922    cx.executor().run_until_parked();
2923    assert_eq!(
2924        visible_entries_as_strings(&panel, 0..10, cx),
2925        &[
2926            "v project_root",
2927            "    > dir_1  <== selected",
2928            "    > dir_2",
2929            "    > dir_3",
2930            "    > dir_4",
2931            "      file_1.py",
2932            "      file_2.py",
2933        ]
2934    );
2935    panel.update_in(cx, |panel, window, cx| {
2936        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2937    });
2938
2939    assert_eq!(
2940        visible_entries_as_strings(&panel, 0..10, cx),
2941        &[
2942            "v project_root  <== selected",
2943            "    > dir_1",
2944            "    > dir_2",
2945            "    > dir_3",
2946            "    > dir_4",
2947            "      file_1.py",
2948            "      file_2.py",
2949        ]
2950    );
2951
2952    panel.update_in(cx, |panel, window, cx| {
2953        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2954    });
2955
2956    assert_eq!(
2957        visible_entries_as_strings(&panel, 0..10, cx),
2958        &[
2959            "v project_root",
2960            "    > dir_1",
2961            "    > dir_2",
2962            "    > dir_3",
2963            "    > dir_4  <== selected",
2964            "      file_1.py",
2965            "      file_2.py",
2966        ]
2967    );
2968
2969    panel.update_in(cx, |panel, window, cx| {
2970        panel.select_next_directory(&SelectNextDirectory, window, cx)
2971    });
2972
2973    assert_eq!(
2974        visible_entries_as_strings(&panel, 0..10, cx),
2975        &[
2976            "v project_root  <== selected",
2977            "    > dir_1",
2978            "    > dir_2",
2979            "    > dir_3",
2980            "    > dir_4",
2981            "      file_1.py",
2982            "      file_2.py",
2983        ]
2984    );
2985}
2986
2987#[gpui::test]
2988async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2989    init_test_with_editor(cx);
2990
2991    let fs = FakeFs::new(cx.executor());
2992    fs.insert_tree(
2993        "/project_root",
2994        json!({
2995            "dir_1": {
2996                "nested_dir": {
2997                    "file_a.py": "# File contents",
2998                }
2999            },
3000            "file_1.py": "# File contents",
3001            "file_2.py": "# File contents",
3002            "zdir_2": {
3003                "nested_dir2": {
3004                    "file_b.py": "# File contents",
3005                }
3006            },
3007        }),
3008    )
3009    .await;
3010
3011    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3012    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3013    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3014    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3015    cx.run_until_parked();
3016
3017    assert_eq!(
3018        visible_entries_as_strings(&panel, 0..10, cx),
3019        &[
3020            "v project_root",
3021            "    > dir_1",
3022            "    > zdir_2",
3023            "      file_1.py",
3024            "      file_2.py",
3025        ]
3026    );
3027    panel.update_in(cx, |panel, window, cx| {
3028        panel.select_first(&SelectFirst, window, cx)
3029    });
3030
3031    assert_eq!(
3032        visible_entries_as_strings(&panel, 0..10, cx),
3033        &[
3034            "v project_root  <== selected",
3035            "    > dir_1",
3036            "    > zdir_2",
3037            "      file_1.py",
3038            "      file_2.py",
3039        ]
3040    );
3041
3042    panel.update_in(cx, |panel, window, cx| {
3043        panel.select_last(&SelectLast, window, cx)
3044    });
3045
3046    assert_eq!(
3047        visible_entries_as_strings(&panel, 0..10, cx),
3048        &[
3049            "v project_root",
3050            "    > dir_1",
3051            "    > zdir_2",
3052            "      file_1.py",
3053            "      file_2.py  <== selected",
3054        ]
3055    );
3056
3057    cx.update(|_, cx| {
3058        let settings = *ProjectPanelSettings::get_global(cx);
3059        ProjectPanelSettings::override_global(
3060            ProjectPanelSettings {
3061                hide_root: true,
3062                ..settings
3063            },
3064            cx,
3065        );
3066    });
3067
3068    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3069    cx.run_until_parked();
3070
3071    #[rustfmt::skip]
3072    assert_eq!(
3073        visible_entries_as_strings(&panel, 0..10, cx),
3074        &[
3075            "> dir_1",
3076            "> zdir_2",
3077            "  file_1.py",
3078            "  file_2.py",
3079        ],
3080        "With hide_root=true, root should be hidden"
3081    );
3082
3083    panel.update_in(cx, |panel, window, cx| {
3084        panel.select_first(&SelectFirst, window, cx)
3085    });
3086
3087    assert_eq!(
3088        visible_entries_as_strings(&panel, 0..10, cx),
3089        &[
3090            "> dir_1  <== selected",
3091            "> zdir_2",
3092            "  file_1.py",
3093            "  file_2.py",
3094        ],
3095        "With hide_root=true, first entry should be dir_1, not the hidden root"
3096    );
3097}
3098
3099#[gpui::test]
3100async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
3101    init_test_with_editor(cx);
3102
3103    let fs = FakeFs::new(cx.executor());
3104    fs.insert_tree(
3105        "/project_root",
3106        json!({
3107            "dir_1": {
3108                "nested_dir": {
3109                    "file_a.py": "# File contents",
3110                }
3111            },
3112            "file_1.py": "# File contents",
3113        }),
3114    )
3115    .await;
3116
3117    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3118    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3119    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3120    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3121    cx.run_until_parked();
3122
3123    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3124    cx.executor().run_until_parked();
3125    select_path(&panel, "project_root/dir_1", cx);
3126    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3127    select_path(&panel, "project_root/dir_1/nested_dir", cx);
3128    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3129    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3130    cx.executor().run_until_parked();
3131    assert_eq!(
3132        visible_entries_as_strings(&panel, 0..10, cx),
3133        &[
3134            "v project_root",
3135            "    v dir_1",
3136            "        > nested_dir  <== selected",
3137            "      file_1.py",
3138        ]
3139    );
3140}
3141
3142#[gpui::test]
3143async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
3144    init_test_with_editor(cx);
3145
3146    let fs = FakeFs::new(cx.executor());
3147    fs.insert_tree(
3148        "/project_root",
3149        json!({
3150            "dir_1": {
3151                "nested_dir": {
3152                    "file_a.py": "# File contents",
3153                    "file_b.py": "# File contents",
3154                    "file_c.py": "# File contents",
3155                },
3156                "file_1.py": "# File contents",
3157                "file_2.py": "# File contents",
3158                "file_3.py": "# File contents",
3159            },
3160            "dir_2": {
3161                "file_1.py": "# File contents",
3162                "file_2.py": "# File contents",
3163                "file_3.py": "# File contents",
3164            }
3165        }),
3166    )
3167    .await;
3168
3169    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3170    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3171    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3172    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3173    cx.run_until_parked();
3174
3175    panel.update_in(cx, |panel, window, cx| {
3176        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3177    });
3178    cx.executor().run_until_parked();
3179    assert_eq!(
3180        visible_entries_as_strings(&panel, 0..10, cx),
3181        &["v project_root", "    > dir_1", "    > dir_2",]
3182    );
3183
3184    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
3185    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3186    cx.executor().run_until_parked();
3187    assert_eq!(
3188        visible_entries_as_strings(&panel, 0..10, cx),
3189        &[
3190            "v project_root",
3191            "    v dir_1  <== selected",
3192            "        > nested_dir",
3193            "          file_1.py",
3194            "          file_2.py",
3195            "          file_3.py",
3196            "    > dir_2",
3197        ]
3198    );
3199}
3200
3201#[gpui::test]
3202async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
3203    init_test_with_editor(cx);
3204
3205    let fs = FakeFs::new(cx.executor());
3206    let worktree_content = json!({
3207        "dir_1": {
3208            "file_1.py": "# File contents",
3209        },
3210        "dir_2": {
3211            "file_1.py": "# File contents",
3212        }
3213    });
3214
3215    fs.insert_tree("/project_root_1", worktree_content.clone())
3216        .await;
3217    fs.insert_tree("/project_root_2", worktree_content).await;
3218
3219    let project = Project::test(
3220        fs.clone(),
3221        ["/project_root_1".as_ref(), "/project_root_2".as_ref()],
3222        cx,
3223    )
3224    .await;
3225    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3226    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3227    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3228    cx.run_until_parked();
3229
3230    panel.update_in(cx, |panel, window, cx| {
3231        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3232    });
3233    cx.executor().run_until_parked();
3234    assert_eq!(
3235        visible_entries_as_strings(&panel, 0..10, cx),
3236        &["> project_root_1", "> project_root_2",]
3237    );
3238}
3239
3240#[gpui::test]
3241async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) {
3242    init_test_with_editor(cx);
3243
3244    let fs = FakeFs::new(cx.executor());
3245    fs.insert_tree(
3246        "/project_root",
3247        json!({
3248            "dir_1": {
3249                "nested_dir": {
3250                    "file_a.py": "# File contents",
3251                    "file_b.py": "# File contents",
3252                    "file_c.py": "# File contents",
3253                },
3254                "file_1.py": "# File contents",
3255                "file_2.py": "# File contents",
3256                "file_3.py": "# File contents",
3257            },
3258            "dir_2": {
3259                "file_1.py": "# File contents",
3260                "file_2.py": "# File contents",
3261                "file_3.py": "# File contents",
3262            }
3263        }),
3264    )
3265    .await;
3266
3267    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3268    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3269    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3270    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3271    cx.run_until_parked();
3272
3273    // Open project_root/dir_1 to ensure that a nested directory is expanded
3274    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3275    cx.executor().run_until_parked();
3276    assert_eq!(
3277        visible_entries_as_strings(&panel, 0..10, cx),
3278        &[
3279            "v project_root",
3280            "    v dir_1  <== selected",
3281            "        > nested_dir",
3282            "          file_1.py",
3283            "          file_2.py",
3284            "          file_3.py",
3285            "    > dir_2",
3286        ]
3287    );
3288
3289    // Close root directory
3290    toggle_expand_dir(&panel, "project_root", cx);
3291    cx.executor().run_until_parked();
3292    assert_eq!(
3293        visible_entries_as_strings(&panel, 0..10, cx),
3294        &["> project_root  <== selected"]
3295    );
3296
3297    // Run collapse_all_entries and make sure root is not expanded
3298    panel.update_in(cx, |panel, window, cx| {
3299        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3300    });
3301    cx.executor().run_until_parked();
3302    assert_eq!(
3303        visible_entries_as_strings(&panel, 0..10, cx),
3304        &["> project_root  <== selected"]
3305    );
3306}
3307
3308#[gpui::test]
3309async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
3310    init_test(cx);
3311
3312    let fs = FakeFs::new(cx.executor());
3313    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
3314    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
3315    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3316    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3317    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3318    cx.run_until_parked();
3319
3320    // Make a new buffer with no backing file
3321    workspace
3322        .update(cx, |workspace, window, cx| {
3323            Editor::new_file(workspace, &Default::default(), window, cx)
3324        })
3325        .unwrap();
3326
3327    cx.executor().run_until_parked();
3328
3329    // "Save as" the buffer, creating a new backing file for it
3330    let save_task = workspace
3331        .update(cx, |workspace, window, cx| {
3332            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3333        })
3334        .unwrap();
3335
3336    cx.executor().run_until_parked();
3337    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
3338    save_task.await.unwrap();
3339
3340    // Rename the file
3341    select_path(&panel, "root/new", cx);
3342    assert_eq!(
3343        visible_entries_as_strings(&panel, 0..10, cx),
3344        &["v root", "      new  <== selected  <== marked"]
3345    );
3346    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3347    panel.update_in(cx, |panel, window, cx| {
3348        panel
3349            .filename_editor
3350            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
3351    });
3352    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3353
3354    cx.executor().run_until_parked();
3355    assert_eq!(
3356        visible_entries_as_strings(&panel, 0..10, cx),
3357        &["v root", "      newer  <== selected"]
3358    );
3359
3360    workspace
3361        .update(cx, |workspace, window, cx| {
3362            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3363        })
3364        .unwrap()
3365        .await
3366        .unwrap();
3367
3368    cx.executor().run_until_parked();
3369    // assert that saving the file doesn't restore "new"
3370    assert_eq!(
3371        visible_entries_as_strings(&panel, 0..10, cx),
3372        &["v root", "      newer  <== selected"]
3373    );
3374}
3375
3376// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
3377// you can't rename a directory which some program has already open. This is a
3378// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
3379// to it, and thus renaming it will fail on Windows.
3380// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
3381// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
3382#[gpui::test]
3383#[cfg_attr(target_os = "windows", ignore)]
3384async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
3385    init_test_with_editor(cx);
3386
3387    let fs = FakeFs::new(cx.executor());
3388    fs.insert_tree(
3389        "/root1",
3390        json!({
3391            "dir1": {
3392                "file1.txt": "content 1",
3393            },
3394        }),
3395    )
3396    .await;
3397
3398    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3399    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3400    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3401    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3402    cx.run_until_parked();
3403
3404    toggle_expand_dir(&panel, "root1/dir1", cx);
3405
3406    assert_eq!(
3407        visible_entries_as_strings(&panel, 0..20, cx),
3408        &["v root1", "    v dir1  <== selected", "          file1.txt",],
3409        "Initial state with worktrees"
3410    );
3411
3412    select_path(&panel, "root1", cx);
3413    assert_eq!(
3414        visible_entries_as_strings(&panel, 0..20, cx),
3415        &["v root1  <== selected", "    v dir1", "          file1.txt",],
3416    );
3417
3418    // Rename root1 to new_root1
3419    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3420
3421    assert_eq!(
3422        visible_entries_as_strings(&panel, 0..20, cx),
3423        &[
3424            "v [EDITOR: 'root1']  <== selected",
3425            "    v dir1",
3426            "          file1.txt",
3427        ],
3428    );
3429
3430    let confirm = panel.update_in(cx, |panel, window, cx| {
3431        panel
3432            .filename_editor
3433            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
3434        panel.confirm_edit(true, window, cx).unwrap()
3435    });
3436    confirm.await.unwrap();
3437    cx.run_until_parked();
3438    assert_eq!(
3439        visible_entries_as_strings(&panel, 0..20, cx),
3440        &[
3441            "v new_root1  <== selected",
3442            "    v dir1",
3443            "          file1.txt",
3444        ],
3445        "Should update worktree name"
3446    );
3447
3448    // Ensure internal paths have been updated
3449    select_path(&panel, "new_root1/dir1/file1.txt", cx);
3450    assert_eq!(
3451        visible_entries_as_strings(&panel, 0..20, cx),
3452        &[
3453            "v new_root1",
3454            "    v dir1",
3455            "          file1.txt  <== selected",
3456        ],
3457        "Files in renamed worktree are selectable"
3458    );
3459}
3460
3461#[gpui::test]
3462async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
3463    init_test_with_editor(cx);
3464
3465    let fs = FakeFs::new(cx.executor());
3466    fs.insert_tree(
3467        "/root1",
3468        json!({
3469            "dir1": { "file1.txt": "content" },
3470            "file2.txt": "content",
3471        }),
3472    )
3473    .await;
3474    fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
3475        .await;
3476
3477    // Test 1: Single worktree, hide_root=true - rename should be blocked
3478    {
3479        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3480        let workspace =
3481            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3482        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3483
3484        cx.update(|_, cx| {
3485            let settings = *ProjectPanelSettings::get_global(cx);
3486            ProjectPanelSettings::override_global(
3487                ProjectPanelSettings {
3488                    hide_root: true,
3489                    ..settings
3490                },
3491                cx,
3492            );
3493        });
3494
3495        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3496        cx.run_until_parked();
3497
3498        panel.update(cx, |panel, cx| {
3499            let project = panel.project.read(cx);
3500            let worktree = project.visible_worktrees(cx).next().unwrap();
3501            let root_entry = worktree.read(cx).root_entry().unwrap();
3502            panel.state.selection = Some(SelectedEntry {
3503                worktree_id: worktree.read(cx).id(),
3504                entry_id: root_entry.id,
3505            });
3506        });
3507
3508        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3509
3510        assert!(
3511            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3512            "Rename should be blocked when hide_root=true with single worktree"
3513        );
3514    }
3515
3516    // Test 2: Multiple worktrees, hide_root=true - rename should work
3517    {
3518        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3519        let workspace =
3520            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3521        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3522
3523        cx.update(|_, cx| {
3524            let settings = *ProjectPanelSettings::get_global(cx);
3525            ProjectPanelSettings::override_global(
3526                ProjectPanelSettings {
3527                    hide_root: true,
3528                    ..settings
3529                },
3530                cx,
3531            );
3532        });
3533
3534        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3535        cx.run_until_parked();
3536
3537        select_path(&panel, "root1", cx);
3538        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3539
3540        #[cfg(target_os = "windows")]
3541        assert!(
3542            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3543            "Rename should be blocked on Windows even with multiple worktrees"
3544        );
3545
3546        #[cfg(not(target_os = "windows"))]
3547        {
3548            assert!(
3549                panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()),
3550                "Rename should work with multiple worktrees on non-Windows when hide_root=true"
3551            );
3552            panel.update_in(cx, |panel, window, cx| {
3553                panel.cancel(&menu::Cancel, window, cx)
3554            });
3555        }
3556    }
3557}
3558
3559#[gpui::test]
3560async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
3561    init_test_with_editor(cx);
3562    let fs = FakeFs::new(cx.executor());
3563    fs.insert_tree(
3564        "/project_root",
3565        json!({
3566            "dir_1": {
3567                "nested_dir": {
3568                    "file_a.py": "# File contents",
3569                }
3570            },
3571            "file_1.py": "# File contents",
3572        }),
3573    )
3574    .await;
3575
3576    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3577    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
3578    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3579    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3580    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3581    cx.run_until_parked();
3582
3583    cx.update(|window, cx| {
3584        panel.update(cx, |this, cx| {
3585            this.select_next(&Default::default(), window, cx);
3586            this.expand_selected_entry(&Default::default(), window, cx);
3587        })
3588    });
3589    cx.run_until_parked();
3590
3591    cx.update(|window, cx| {
3592        panel.update(cx, |this, cx| {
3593            this.expand_selected_entry(&Default::default(), window, cx);
3594        })
3595    });
3596    cx.run_until_parked();
3597
3598    cx.update(|window, cx| {
3599        panel.update(cx, |this, cx| {
3600            this.select_next(&Default::default(), window, cx);
3601            this.expand_selected_entry(&Default::default(), window, cx);
3602        })
3603    });
3604    cx.run_until_parked();
3605
3606    cx.update(|window, cx| {
3607        panel.update(cx, |this, cx| {
3608            this.select_next(&Default::default(), window, cx);
3609        })
3610    });
3611    cx.run_until_parked();
3612
3613    assert_eq!(
3614        visible_entries_as_strings(&panel, 0..10, cx),
3615        &[
3616            "v project_root",
3617            "    v dir_1",
3618            "        v nested_dir",
3619            "              file_a.py  <== selected",
3620            "      file_1.py",
3621        ]
3622    );
3623    let modifiers_with_shift = gpui::Modifiers {
3624        shift: true,
3625        ..Default::default()
3626    };
3627    cx.run_until_parked();
3628    cx.simulate_modifiers_change(modifiers_with_shift);
3629    cx.update(|window, cx| {
3630        panel.update(cx, |this, cx| {
3631            this.select_next(&Default::default(), window, cx);
3632        })
3633    });
3634    assert_eq!(
3635        visible_entries_as_strings(&panel, 0..10, cx),
3636        &[
3637            "v project_root",
3638            "    v dir_1",
3639            "        v nested_dir",
3640            "              file_a.py",
3641            "      file_1.py  <== selected  <== marked",
3642        ]
3643    );
3644    cx.update(|window, cx| {
3645        panel.update(cx, |this, cx| {
3646            this.select_previous(&Default::default(), window, cx);
3647        })
3648    });
3649    assert_eq!(
3650        visible_entries_as_strings(&panel, 0..10, cx),
3651        &[
3652            "v project_root",
3653            "    v dir_1",
3654            "        v nested_dir",
3655            "              file_a.py  <== selected  <== marked",
3656            "      file_1.py  <== marked",
3657        ]
3658    );
3659    cx.update(|window, cx| {
3660        panel.update(cx, |this, cx| {
3661            let drag = DraggedSelection {
3662                active_selection: this.state.selection.unwrap(),
3663                marked_selections: this.marked_entries.clone().into(),
3664            };
3665            let target_entry = this
3666                .project
3667                .read(cx)
3668                .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
3669                .unwrap();
3670            this.drag_onto(&drag, target_entry.id, false, window, cx);
3671        });
3672    });
3673    cx.run_until_parked();
3674    assert_eq!(
3675        visible_entries_as_strings(&panel, 0..10, cx),
3676        &[
3677            "v project_root",
3678            "    v dir_1",
3679            "        v nested_dir",
3680            "      file_1.py  <== marked",
3681            "      file_a.py  <== selected  <== marked",
3682        ]
3683    );
3684    // ESC clears out all marks
3685    cx.update(|window, cx| {
3686        panel.update(cx, |this, cx| {
3687            this.cancel(&menu::Cancel, window, cx);
3688        })
3689    });
3690    assert_eq!(
3691        visible_entries_as_strings(&panel, 0..10, cx),
3692        &[
3693            "v project_root",
3694            "    v dir_1",
3695            "        v nested_dir",
3696            "      file_1.py",
3697            "      file_a.py  <== selected",
3698        ]
3699    );
3700    // ESC clears out all marks
3701    cx.update(|window, cx| {
3702        panel.update(cx, |this, cx| {
3703            this.select_previous(&SelectPrevious, window, cx);
3704            this.select_next(&SelectNext, window, cx);
3705        })
3706    });
3707    assert_eq!(
3708        visible_entries_as_strings(&panel, 0..10, cx),
3709        &[
3710            "v project_root",
3711            "    v dir_1",
3712            "        v nested_dir",
3713            "      file_1.py  <== marked",
3714            "      file_a.py  <== selected  <== marked",
3715        ]
3716    );
3717    cx.simulate_modifiers_change(Default::default());
3718    cx.update(|window, cx| {
3719        panel.update(cx, |this, cx| {
3720            this.cut(&Cut, window, cx);
3721            this.select_previous(&SelectPrevious, window, cx);
3722            this.select_previous(&SelectPrevious, window, cx);
3723
3724            this.paste(&Paste, window, cx);
3725            this.update_visible_entries(None, false, false, window, cx);
3726        })
3727    });
3728    cx.run_until_parked();
3729    assert_eq!(
3730        visible_entries_as_strings(&panel, 0..10, cx),
3731        &[
3732            "v project_root",
3733            "    v dir_1",
3734            "        v nested_dir",
3735            "              file_1.py  <== marked",
3736            "              file_a.py  <== selected  <== marked",
3737        ]
3738    );
3739    cx.simulate_modifiers_change(modifiers_with_shift);
3740    cx.update(|window, cx| {
3741        panel.update(cx, |this, cx| {
3742            this.expand_selected_entry(&Default::default(), window, cx);
3743            this.select_next(&SelectNext, window, cx);
3744            this.select_next(&SelectNext, window, cx);
3745        })
3746    });
3747    submit_deletion(&panel, cx);
3748    assert_eq!(
3749        visible_entries_as_strings(&panel, 0..10, cx),
3750        &[
3751            "v project_root",
3752            "    v dir_1",
3753            "        v nested_dir  <== selected",
3754        ]
3755    );
3756}
3757
3758#[gpui::test]
3759async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
3760    init_test(cx);
3761
3762    let fs = FakeFs::new(cx.executor());
3763    fs.insert_tree(
3764        "/root",
3765        json!({
3766            "a": {
3767                "b": {
3768                    "c": {
3769                        "d": {}
3770                    }
3771                }
3772            },
3773            "target_destination": {}
3774        }),
3775    )
3776    .await;
3777
3778    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3779    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3780    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3781
3782    cx.update(|_, cx| {
3783        let settings = *ProjectPanelSettings::get_global(cx);
3784        ProjectPanelSettings::override_global(
3785            ProjectPanelSettings {
3786                auto_fold_dirs: true,
3787                ..settings
3788            },
3789            cx,
3790        );
3791    });
3792
3793    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3794    cx.run_until_parked();
3795
3796    // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
3797    select_path(&panel, "root/a/b/c/d", cx);
3798    panel.update_in(cx, |panel, window, cx| {
3799        let drag = DraggedSelection {
3800            active_selection: SelectedEntry {
3801                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
3802                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
3803            },
3804            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
3805        };
3806        let target_entry = panel
3807            .project
3808            .read(cx)
3809            .visible_worktrees(cx)
3810            .next()
3811            .unwrap()
3812            .read(cx)
3813            .entry_for_path(rel_path("target_destination"))
3814            .unwrap();
3815        panel.drag_onto(&drag, target_entry.id, false, window, cx);
3816    });
3817    cx.executor().run_until_parked();
3818
3819    assert_eq!(
3820        visible_entries_as_strings(&panel, 0..10, cx),
3821        &[
3822            "v root",
3823            "    > a/b/c",
3824            "    > target_destination/d  <== selected"
3825        ],
3826        "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
3827    );
3828
3829    // Reset
3830    select_path(&panel, "root/target_destination/d", cx);
3831    panel.update_in(cx, |panel, window, cx| {
3832        let drag = DraggedSelection {
3833            active_selection: SelectedEntry {
3834                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
3835                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
3836            },
3837            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
3838        };
3839        let target_entry = panel
3840            .project
3841            .read(cx)
3842            .visible_worktrees(cx)
3843            .next()
3844            .unwrap()
3845            .read(cx)
3846            .entry_for_path(rel_path("a/b/c"))
3847            .unwrap();
3848        panel.drag_onto(&drag, target_entry.id, false, window, cx);
3849    });
3850    cx.executor().run_until_parked();
3851
3852    // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
3853    select_path(&panel, "root/a/b", cx);
3854    panel.update_in(cx, |panel, window, cx| {
3855        let drag = DraggedSelection {
3856            active_selection: SelectedEntry {
3857                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
3858                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
3859            },
3860            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
3861        };
3862        let target_entry = panel
3863            .project
3864            .read(cx)
3865            .visible_worktrees(cx)
3866            .next()
3867            .unwrap()
3868            .read(cx)
3869            .entry_for_path(rel_path("target_destination"))
3870            .unwrap();
3871        panel.drag_onto(&drag, target_entry.id, false, window, cx);
3872    });
3873    cx.executor().run_until_parked();
3874
3875    assert_eq!(
3876        visible_entries_as_strings(&panel, 0..10, cx),
3877        &["v root", "    v a", "    > target_destination/b/c/d"],
3878        "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
3879    );
3880
3881    // Reset
3882    select_path(&panel, "root/target_destination/b", cx);
3883    panel.update_in(cx, |panel, window, cx| {
3884        let drag = DraggedSelection {
3885            active_selection: SelectedEntry {
3886                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
3887                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
3888            },
3889            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
3890        };
3891        let target_entry = panel
3892            .project
3893            .read(cx)
3894            .visible_worktrees(cx)
3895            .next()
3896            .unwrap()
3897            .read(cx)
3898            .entry_for_path(rel_path("a"))
3899            .unwrap();
3900        panel.drag_onto(&drag, target_entry.id, false, window, cx);
3901    });
3902    cx.executor().run_until_parked();
3903
3904    // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
3905    select_path(&panel, "root/a", cx);
3906    panel.update_in(cx, |panel, window, cx| {
3907        let drag = DraggedSelection {
3908            active_selection: SelectedEntry {
3909                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
3910                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
3911            },
3912            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
3913        };
3914        let target_entry = panel
3915            .project
3916            .read(cx)
3917            .visible_worktrees(cx)
3918            .next()
3919            .unwrap()
3920            .read(cx)
3921            .entry_for_path(rel_path("target_destination"))
3922            .unwrap();
3923        panel.drag_onto(&drag, target_entry.id, false, window, cx);
3924    });
3925    cx.executor().run_until_parked();
3926
3927    assert_eq!(
3928        visible_entries_as_strings(&panel, 0..10, cx),
3929        &["v root", "    > target_destination/a/b/c/d"],
3930        "Moving first directory 'a' should move whole 'a/b/c/d' chain"
3931    );
3932}
3933
3934#[gpui::test]
3935async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
3936    init_test_with_editor(cx);
3937    cx.update(|cx| {
3938        cx.update_global::<SettingsStore, _>(|store, cx| {
3939            store.update_user_settings(cx, |settings| {
3940                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
3941                settings
3942                    .project_panel
3943                    .get_or_insert_default()
3944                    .auto_reveal_entries = Some(false);
3945            });
3946        })
3947    });
3948
3949    let fs = FakeFs::new(cx.background_executor.clone());
3950    fs.insert_tree(
3951        "/project_root",
3952        json!({
3953            ".git": {},
3954            ".gitignore": "**/gitignored_dir",
3955            "dir_1": {
3956                "file_1.py": "# File 1_1 contents",
3957                "file_2.py": "# File 1_2 contents",
3958                "file_3.py": "# File 1_3 contents",
3959                "gitignored_dir": {
3960                    "file_a.py": "# File contents",
3961                    "file_b.py": "# File contents",
3962                    "file_c.py": "# File contents",
3963                },
3964            },
3965            "dir_2": {
3966                "file_1.py": "# File 2_1 contents",
3967                "file_2.py": "# File 2_2 contents",
3968                "file_3.py": "# File 2_3 contents",
3969            }
3970        }),
3971    )
3972    .await;
3973
3974    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3975    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3976    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3977    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3978    cx.run_until_parked();
3979
3980    assert_eq!(
3981        visible_entries_as_strings(&panel, 0..20, cx),
3982        &[
3983            "v project_root",
3984            "    > .git",
3985            "    > dir_1",
3986            "    > dir_2",
3987            "      .gitignore",
3988        ]
3989    );
3990
3991    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3992        .expect("dir 1 file is not ignored and should have an entry");
3993    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3994        .expect("dir 2 file is not ignored and should have an entry");
3995    let gitignored_dir_file =
3996        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3997    assert_eq!(
3998        gitignored_dir_file, None,
3999        "File in the gitignored dir should not have an entry before its dir is toggled"
4000    );
4001
4002    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4003    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4004    cx.executor().run_until_parked();
4005    assert_eq!(
4006        visible_entries_as_strings(&panel, 0..20, cx),
4007        &[
4008            "v project_root",
4009            "    > .git",
4010            "    v dir_1",
4011            "        v gitignored_dir  <== selected",
4012            "              file_a.py",
4013            "              file_b.py",
4014            "              file_c.py",
4015            "          file_1.py",
4016            "          file_2.py",
4017            "          file_3.py",
4018            "    > dir_2",
4019            "      .gitignore",
4020        ],
4021        "Should show gitignored dir file list in the project panel"
4022    );
4023    let gitignored_dir_file =
4024        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4025            .expect("after gitignored dir got opened, a file entry should be present");
4026
4027    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4028    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4029    assert_eq!(
4030        visible_entries_as_strings(&panel, 0..20, cx),
4031        &[
4032            "v project_root",
4033            "    > .git",
4034            "    > dir_1  <== selected",
4035            "    > dir_2",
4036            "      .gitignore",
4037        ],
4038        "Should hide all dir contents again and prepare for the auto reveal test"
4039    );
4040
4041    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4042        panel.update(cx, |panel, cx| {
4043            panel.project.update(cx, |_, cx| {
4044                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4045            })
4046        });
4047        cx.run_until_parked();
4048        assert_eq!(
4049            visible_entries_as_strings(&panel, 0..20, cx),
4050            &[
4051                "v project_root",
4052                "    > .git",
4053                "    > dir_1  <== selected",
4054                "    > dir_2",
4055                "      .gitignore",
4056            ],
4057            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4058        );
4059    }
4060
4061    cx.update(|_, cx| {
4062        cx.update_global::<SettingsStore, _>(|store, cx| {
4063            store.update_user_settings(cx, |settings| {
4064                settings
4065                    .project_panel
4066                    .get_or_insert_default()
4067                    .auto_reveal_entries = Some(true)
4068            });
4069        })
4070    });
4071
4072    panel.update(cx, |panel, cx| {
4073        panel.project.update(cx, |_, cx| {
4074            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
4075        })
4076    });
4077    cx.run_until_parked();
4078    assert_eq!(
4079        visible_entries_as_strings(&panel, 0..20, cx),
4080        &[
4081            "v project_root",
4082            "    > .git",
4083            "    v dir_1",
4084            "        > gitignored_dir",
4085            "          file_1.py  <== selected  <== marked",
4086            "          file_2.py",
4087            "          file_3.py",
4088            "    > dir_2",
4089            "      .gitignore",
4090        ],
4091        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
4092    );
4093
4094    panel.update(cx, |panel, cx| {
4095        panel.project.update(cx, |_, cx| {
4096            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
4097        })
4098    });
4099    cx.run_until_parked();
4100    assert_eq!(
4101        visible_entries_as_strings(&panel, 0..20, cx),
4102        &[
4103            "v project_root",
4104            "    > .git",
4105            "    v dir_1",
4106            "        > gitignored_dir",
4107            "          file_1.py",
4108            "          file_2.py",
4109            "          file_3.py",
4110            "    v dir_2",
4111            "          file_1.py  <== selected  <== marked",
4112            "          file_2.py",
4113            "          file_3.py",
4114            "      .gitignore",
4115        ],
4116        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
4117    );
4118
4119    panel.update(cx, |panel, cx| {
4120        panel.project.update(cx, |_, cx| {
4121            cx.emit(project::Event::ActiveEntryChanged(Some(
4122                gitignored_dir_file,
4123            )))
4124        })
4125    });
4126    cx.run_until_parked();
4127    assert_eq!(
4128        visible_entries_as_strings(&panel, 0..20, cx),
4129        &[
4130            "v project_root",
4131            "    > .git",
4132            "    v dir_1",
4133            "        > gitignored_dir",
4134            "          file_1.py",
4135            "          file_2.py",
4136            "          file_3.py",
4137            "    v dir_2",
4138            "          file_1.py  <== selected  <== marked",
4139            "          file_2.py",
4140            "          file_3.py",
4141            "      .gitignore",
4142        ],
4143        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
4144    );
4145
4146    panel.update(cx, |panel, cx| {
4147        panel.project.update(cx, |_, cx| {
4148            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4149        })
4150    });
4151    cx.run_until_parked();
4152    assert_eq!(
4153        visible_entries_as_strings(&panel, 0..20, cx),
4154        &[
4155            "v project_root",
4156            "    > .git",
4157            "    v dir_1",
4158            "        v gitignored_dir",
4159            "              file_a.py  <== selected  <== marked",
4160            "              file_b.py",
4161            "              file_c.py",
4162            "          file_1.py",
4163            "          file_2.py",
4164            "          file_3.py",
4165            "    v dir_2",
4166            "          file_1.py",
4167            "          file_2.py",
4168            "          file_3.py",
4169            "      .gitignore",
4170        ],
4171        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
4172    );
4173}
4174
4175#[gpui::test]
4176async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
4177    init_test_with_editor(cx);
4178    cx.update(|cx| {
4179        cx.update_global::<SettingsStore, _>(|store, cx| {
4180            store.update_user_settings(cx, |settings| {
4181                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4182                settings.project.worktree.file_scan_inclusions =
4183                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
4184                settings
4185                    .project_panel
4186                    .get_or_insert_default()
4187                    .auto_reveal_entries = Some(false)
4188            });
4189        })
4190    });
4191
4192    let fs = FakeFs::new(cx.background_executor.clone());
4193    fs.insert_tree(
4194        "/project_root",
4195        json!({
4196            ".git": {},
4197            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
4198            "dir_1": {
4199                "file_1.py": "# File 1_1 contents",
4200                "file_2.py": "# File 1_2 contents",
4201                "file_3.py": "# File 1_3 contents",
4202                "gitignored_dir": {
4203                    "file_a.py": "# File contents",
4204                    "file_b.py": "# File contents",
4205                    "file_c.py": "# File contents",
4206                },
4207            },
4208            "dir_2": {
4209                "file_1.py": "# File 2_1 contents",
4210                "file_2.py": "# File 2_2 contents",
4211                "file_3.py": "# File 2_3 contents",
4212            },
4213            "always_included_but_ignored_dir": {
4214                "file_a.py": "# File contents",
4215                "file_b.py": "# File contents",
4216                "file_c.py": "# File contents",
4217            },
4218        }),
4219    )
4220    .await;
4221
4222    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4223    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4224    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4225    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4226    cx.run_until_parked();
4227
4228    assert_eq!(
4229        visible_entries_as_strings(&panel, 0..20, cx),
4230        &[
4231            "v project_root",
4232            "    > .git",
4233            "    > always_included_but_ignored_dir",
4234            "    > dir_1",
4235            "    > dir_2",
4236            "      .gitignore",
4237        ]
4238    );
4239
4240    let gitignored_dir_file =
4241        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4242    let always_included_but_ignored_dir_file = find_project_entry(
4243        &panel,
4244        "project_root/always_included_but_ignored_dir/file_a.py",
4245        cx,
4246    )
4247    .expect("file that is .gitignored but set to always be included should have an entry");
4248    assert_eq!(
4249        gitignored_dir_file, None,
4250        "File in the gitignored dir should not have an entry unless its directory is toggled"
4251    );
4252
4253    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4254    cx.run_until_parked();
4255    cx.update(|_, cx| {
4256        cx.update_global::<SettingsStore, _>(|store, cx| {
4257            store.update_user_settings(cx, |settings| {
4258                settings
4259                    .project_panel
4260                    .get_or_insert_default()
4261                    .auto_reveal_entries = Some(true)
4262            });
4263        })
4264    });
4265
4266    panel.update(cx, |panel, cx| {
4267        panel.project.update(cx, |_, cx| {
4268            cx.emit(project::Event::ActiveEntryChanged(Some(
4269                always_included_but_ignored_dir_file,
4270            )))
4271        })
4272    });
4273    cx.run_until_parked();
4274
4275    assert_eq!(
4276        visible_entries_as_strings(&panel, 0..20, cx),
4277        &[
4278            "v project_root",
4279            "    > .git",
4280            "    v always_included_but_ignored_dir",
4281            "          file_a.py  <== selected  <== marked",
4282            "          file_b.py",
4283            "          file_c.py",
4284            "    v dir_1",
4285            "        > gitignored_dir",
4286            "          file_1.py",
4287            "          file_2.py",
4288            "          file_3.py",
4289            "    > dir_2",
4290            "      .gitignore",
4291        ],
4292        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
4293    );
4294}
4295
4296#[gpui::test]
4297async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
4298    init_test_with_editor(cx);
4299    cx.update(|cx| {
4300        cx.update_global::<SettingsStore, _>(|store, cx| {
4301            store.update_user_settings(cx, |settings| {
4302                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4303                settings
4304                    .project_panel
4305                    .get_or_insert_default()
4306                    .auto_reveal_entries = Some(false)
4307            });
4308        })
4309    });
4310
4311    let fs = FakeFs::new(cx.background_executor.clone());
4312    fs.insert_tree(
4313        "/project_root",
4314        json!({
4315            ".git": {},
4316            ".gitignore": "**/gitignored_dir",
4317            "dir_1": {
4318                "file_1.py": "# File 1_1 contents",
4319                "file_2.py": "# File 1_2 contents",
4320                "file_3.py": "# File 1_3 contents",
4321                "gitignored_dir": {
4322                    "file_a.py": "# File contents",
4323                    "file_b.py": "# File contents",
4324                    "file_c.py": "# File contents",
4325                },
4326            },
4327            "dir_2": {
4328                "file_1.py": "# File 2_1 contents",
4329                "file_2.py": "# File 2_2 contents",
4330                "file_3.py": "# File 2_3 contents",
4331            }
4332        }),
4333    )
4334    .await;
4335
4336    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4337    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4338    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4339    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4340    cx.run_until_parked();
4341
4342    assert_eq!(
4343        visible_entries_as_strings(&panel, 0..20, cx),
4344        &[
4345            "v project_root",
4346            "    > .git",
4347            "    > dir_1",
4348            "    > dir_2",
4349            "      .gitignore",
4350        ]
4351    );
4352
4353    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4354        .expect("dir 1 file is not ignored and should have an entry");
4355    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4356        .expect("dir 2 file is not ignored and should have an entry");
4357    let gitignored_dir_file =
4358        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4359    assert_eq!(
4360        gitignored_dir_file, None,
4361        "File in the gitignored dir should not have an entry before its dir is toggled"
4362    );
4363
4364    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4365    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4366    cx.run_until_parked();
4367    assert_eq!(
4368        visible_entries_as_strings(&panel, 0..20, cx),
4369        &[
4370            "v project_root",
4371            "    > .git",
4372            "    v dir_1",
4373            "        v gitignored_dir  <== selected",
4374            "              file_a.py",
4375            "              file_b.py",
4376            "              file_c.py",
4377            "          file_1.py",
4378            "          file_2.py",
4379            "          file_3.py",
4380            "    > dir_2",
4381            "      .gitignore",
4382        ],
4383        "Should show gitignored dir file list in the project panel"
4384    );
4385    let gitignored_dir_file =
4386        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4387            .expect("after gitignored dir got opened, a file entry should be present");
4388
4389    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4390    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4391    assert_eq!(
4392        visible_entries_as_strings(&panel, 0..20, cx),
4393        &[
4394            "v project_root",
4395            "    > .git",
4396            "    > dir_1  <== selected",
4397            "    > dir_2",
4398            "      .gitignore",
4399        ],
4400        "Should hide all dir contents again and prepare for the explicit reveal test"
4401    );
4402
4403    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4404        panel.update(cx, |panel, cx| {
4405            panel.project.update(cx, |_, cx| {
4406                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4407            })
4408        });
4409        cx.run_until_parked();
4410        assert_eq!(
4411            visible_entries_as_strings(&panel, 0..20, cx),
4412            &[
4413                "v project_root",
4414                "    > .git",
4415                "    > dir_1  <== selected",
4416                "    > dir_2",
4417                "      .gitignore",
4418            ],
4419            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4420        );
4421    }
4422
4423    panel.update(cx, |panel, cx| {
4424        panel.project.update(cx, |_, cx| {
4425            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
4426        })
4427    });
4428    cx.run_until_parked();
4429    assert_eq!(
4430        visible_entries_as_strings(&panel, 0..20, cx),
4431        &[
4432            "v project_root",
4433            "    > .git",
4434            "    v dir_1",
4435            "        > gitignored_dir",
4436            "          file_1.py  <== selected  <== marked",
4437            "          file_2.py",
4438            "          file_3.py",
4439            "    > dir_2",
4440            "      .gitignore",
4441        ],
4442        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
4443    );
4444
4445    panel.update(cx, |panel, cx| {
4446        panel.project.update(cx, |_, cx| {
4447            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
4448        })
4449    });
4450    cx.run_until_parked();
4451    assert_eq!(
4452        visible_entries_as_strings(&panel, 0..20, cx),
4453        &[
4454            "v project_root",
4455            "    > .git",
4456            "    v dir_1",
4457            "        > gitignored_dir",
4458            "          file_1.py",
4459            "          file_2.py",
4460            "          file_3.py",
4461            "    v dir_2",
4462            "          file_1.py  <== selected  <== marked",
4463            "          file_2.py",
4464            "          file_3.py",
4465            "      .gitignore",
4466        ],
4467        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
4468    );
4469
4470    panel.update(cx, |panel, cx| {
4471        panel.project.update(cx, |_, cx| {
4472            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4473        })
4474    });
4475    cx.run_until_parked();
4476    assert_eq!(
4477        visible_entries_as_strings(&panel, 0..20, cx),
4478        &[
4479            "v project_root",
4480            "    > .git",
4481            "    v dir_1",
4482            "        v gitignored_dir",
4483            "              file_a.py  <== selected  <== marked",
4484            "              file_b.py",
4485            "              file_c.py",
4486            "          file_1.py",
4487            "          file_2.py",
4488            "          file_3.py",
4489            "    v dir_2",
4490            "          file_1.py",
4491            "          file_2.py",
4492            "          file_3.py",
4493            "      .gitignore",
4494        ],
4495        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
4496    );
4497}
4498
4499#[gpui::test]
4500async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
4501    init_test(cx);
4502    cx.update(|cx| {
4503        cx.update_global::<SettingsStore, _>(|store, cx| {
4504            store.update_user_settings(cx, |settings| {
4505                settings.project.worktree.file_scan_exclusions =
4506                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
4507            });
4508        });
4509    });
4510
4511    cx.update(|cx| {
4512        register_project_item::<TestProjectItemView>(cx);
4513    });
4514
4515    let fs = FakeFs::new(cx.executor());
4516    fs.insert_tree(
4517        "/root1",
4518        json!({
4519            ".dockerignore": "",
4520            ".git": {
4521                "HEAD": "",
4522            },
4523        }),
4524    )
4525    .await;
4526
4527    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4528    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4529    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4530    let panel = workspace
4531        .update(cx, |workspace, window, cx| {
4532            let panel = ProjectPanel::new(workspace, window, cx);
4533            workspace.add_panel(panel.clone(), window, cx);
4534            panel
4535        })
4536        .unwrap();
4537    cx.run_until_parked();
4538
4539    select_path(&panel, "root1", cx);
4540    assert_eq!(
4541        visible_entries_as_strings(&panel, 0..10, cx),
4542        &["v root1  <== selected", "      .dockerignore",]
4543    );
4544    workspace
4545        .update(cx, |workspace, _, cx| {
4546            assert!(
4547                workspace.active_item(cx).is_none(),
4548                "Should have no active items in the beginning"
4549            );
4550        })
4551        .unwrap();
4552
4553    let excluded_file_path = ".git/COMMIT_EDITMSG";
4554    let excluded_dir_path = "excluded_dir";
4555
4556    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
4557    cx.run_until_parked();
4558    panel.update_in(cx, |panel, window, cx| {
4559        assert!(panel.filename_editor.read(cx).is_focused(window));
4560    });
4561    panel
4562        .update_in(cx, |panel, window, cx| {
4563            panel.filename_editor.update(cx, |editor, cx| {
4564                editor.set_text(excluded_file_path, window, cx)
4565            });
4566            panel.confirm_edit(true, window, cx).unwrap()
4567        })
4568        .await
4569        .unwrap();
4570
4571    assert_eq!(
4572        visible_entries_as_strings(&panel, 0..13, cx),
4573        &["v root1", "      .dockerignore"],
4574        "Excluded dir should not be shown after opening a file in it"
4575    );
4576    panel.update_in(cx, |panel, window, cx| {
4577        assert!(
4578            !panel.filename_editor.read(cx).is_focused(window),
4579            "Should have closed the file name editor"
4580        );
4581    });
4582    workspace
4583        .update(cx, |workspace, _, cx| {
4584            let active_entry_path = workspace
4585                .active_item(cx)
4586                .expect("should have opened and activated the excluded item")
4587                .act_as::<TestProjectItemView>(cx)
4588                .expect("should have opened the corresponding project item for the excluded item")
4589                .read(cx)
4590                .path
4591                .clone();
4592            assert_eq!(
4593                active_entry_path.path.as_ref(),
4594                rel_path(excluded_file_path),
4595                "Should open the excluded file"
4596            );
4597
4598            assert!(
4599                workspace.notification_ids().is_empty(),
4600                "Should have no notifications after opening an excluded file"
4601            );
4602        })
4603        .unwrap();
4604    assert!(
4605        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
4606        "Should have created the excluded file"
4607    );
4608
4609    select_path(&panel, "root1", cx);
4610    panel.update_in(cx, |panel, window, cx| {
4611        panel.new_directory(&NewDirectory, window, cx)
4612    });
4613    cx.run_until_parked();
4614    panel.update_in(cx, |panel, window, cx| {
4615        assert!(panel.filename_editor.read(cx).is_focused(window));
4616    });
4617    panel
4618        .update_in(cx, |panel, window, cx| {
4619            panel.filename_editor.update(cx, |editor, cx| {
4620                editor.set_text(excluded_file_path, window, cx)
4621            });
4622            panel.confirm_edit(true, window, cx).unwrap()
4623        })
4624        .await
4625        .unwrap();
4626    cx.run_until_parked();
4627    assert_eq!(
4628        visible_entries_as_strings(&panel, 0..13, cx),
4629        &["v root1", "      .dockerignore"],
4630        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
4631    );
4632    panel.update_in(cx, |panel, window, cx| {
4633        assert!(
4634            !panel.filename_editor.read(cx).is_focused(window),
4635            "Should have closed the file name editor"
4636        );
4637    });
4638    workspace
4639        .update(cx, |workspace, _, cx| {
4640            let notifications = workspace.notification_ids();
4641            assert_eq!(
4642                notifications.len(),
4643                1,
4644                "Should receive one notification with the error message"
4645            );
4646            workspace.dismiss_notification(notifications.first().unwrap(), cx);
4647            assert!(workspace.notification_ids().is_empty());
4648        })
4649        .unwrap();
4650
4651    select_path(&panel, "root1", cx);
4652    panel.update_in(cx, |panel, window, cx| {
4653        panel.new_directory(&NewDirectory, window, cx)
4654    });
4655    cx.run_until_parked();
4656
4657    panel.update_in(cx, |panel, window, cx| {
4658        assert!(panel.filename_editor.read(cx).is_focused(window));
4659    });
4660
4661    panel
4662        .update_in(cx, |panel, window, cx| {
4663            panel.filename_editor.update(cx, |editor, cx| {
4664                editor.set_text(excluded_dir_path, window, cx)
4665            });
4666            panel.confirm_edit(true, window, cx).unwrap()
4667        })
4668        .await
4669        .unwrap();
4670
4671    cx.run_until_parked();
4672
4673    assert_eq!(
4674        visible_entries_as_strings(&panel, 0..13, cx),
4675        &["v root1", "      .dockerignore"],
4676        "Should not change the project panel after trying to create an excluded directory"
4677    );
4678    panel.update_in(cx, |panel, window, cx| {
4679        assert!(
4680            !panel.filename_editor.read(cx).is_focused(window),
4681            "Should have closed the file name editor"
4682        );
4683    });
4684    workspace
4685        .update(cx, |workspace, _, cx| {
4686            let notifications = workspace.notification_ids();
4687            assert_eq!(
4688                notifications.len(),
4689                1,
4690                "Should receive one notification explaining that no directory is actually shown"
4691            );
4692            workspace.dismiss_notification(notifications.first().unwrap(), cx);
4693            assert!(workspace.notification_ids().is_empty());
4694        })
4695        .unwrap();
4696    assert!(
4697        fs.is_dir(Path::new("/root1/excluded_dir")).await,
4698        "Should have created the excluded directory"
4699    );
4700}
4701
4702#[gpui::test]
4703async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
4704    init_test_with_editor(cx);
4705
4706    let fs = FakeFs::new(cx.executor());
4707    fs.insert_tree(
4708        "/src",
4709        json!({
4710            "test": {
4711                "first.rs": "// First Rust file",
4712                "second.rs": "// Second Rust file",
4713                "third.rs": "// Third Rust file",
4714            }
4715        }),
4716    )
4717    .await;
4718
4719    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
4720    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4721    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4722    let panel = workspace
4723        .update(cx, |workspace, window, cx| {
4724            let panel = ProjectPanel::new(workspace, window, cx);
4725            workspace.add_panel(panel.clone(), window, cx);
4726            panel
4727        })
4728        .unwrap();
4729    cx.run_until_parked();
4730
4731    select_path(&panel, "src", cx);
4732    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
4733    cx.executor().run_until_parked();
4734    assert_eq!(
4735        visible_entries_as_strings(&panel, 0..10, cx),
4736        &[
4737            //
4738            "v src  <== selected",
4739            "    > test"
4740        ]
4741    );
4742    panel.update_in(cx, |panel, window, cx| {
4743        panel.new_directory(&NewDirectory, window, cx)
4744    });
4745    cx.executor().run_until_parked();
4746    panel.update_in(cx, |panel, window, cx| {
4747        assert!(panel.filename_editor.read(cx).is_focused(window));
4748    });
4749    assert_eq!(
4750        visible_entries_as_strings(&panel, 0..10, cx),
4751        &[
4752            //
4753            "v src",
4754            "    > [EDITOR: '']  <== selected",
4755            "    > test"
4756        ]
4757    );
4758
4759    panel.update_in(cx, |panel, window, cx| {
4760        panel.cancel(&menu::Cancel, window, cx);
4761        panel.update_visible_entries(None, false, false, window, cx);
4762    });
4763    cx.executor().run_until_parked();
4764    assert_eq!(
4765        visible_entries_as_strings(&panel, 0..10, cx),
4766        &[
4767            //
4768            "v src  <== selected",
4769            "    > test"
4770        ]
4771    );
4772}
4773
4774#[gpui::test]
4775async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
4776    init_test_with_editor(cx);
4777
4778    let fs = FakeFs::new(cx.executor());
4779    fs.insert_tree(
4780        "/root",
4781        json!({
4782            "dir1": {
4783                "subdir1": {},
4784                "file1.txt": "",
4785                "file2.txt": "",
4786            },
4787            "dir2": {
4788                "subdir2": {},
4789                "file3.txt": "",
4790                "file4.txt": "",
4791            },
4792            "file5.txt": "",
4793            "file6.txt": "",
4794        }),
4795    )
4796    .await;
4797
4798    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4799    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4800    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4801    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4802    cx.run_until_parked();
4803
4804    toggle_expand_dir(&panel, "root/dir1", cx);
4805    toggle_expand_dir(&panel, "root/dir2", cx);
4806
4807    // Test Case 1: Delete middle file in directory
4808    select_path(&panel, "root/dir1/file1.txt", cx);
4809    assert_eq!(
4810        visible_entries_as_strings(&panel, 0..15, cx),
4811        &[
4812            "v root",
4813            "    v dir1",
4814            "        > subdir1",
4815            "          file1.txt  <== selected",
4816            "          file2.txt",
4817            "    v dir2",
4818            "        > subdir2",
4819            "          file3.txt",
4820            "          file4.txt",
4821            "      file5.txt",
4822            "      file6.txt",
4823        ],
4824        "Initial state before deleting middle file"
4825    );
4826
4827    submit_deletion(&panel, cx);
4828    assert_eq!(
4829        visible_entries_as_strings(&panel, 0..15, cx),
4830        &[
4831            "v root",
4832            "    v dir1",
4833            "        > subdir1",
4834            "          file2.txt  <== selected",
4835            "    v dir2",
4836            "        > subdir2",
4837            "          file3.txt",
4838            "          file4.txt",
4839            "      file5.txt",
4840            "      file6.txt",
4841        ],
4842        "Should select next file after deleting middle file"
4843    );
4844
4845    // Test Case 2: Delete last file in directory
4846    submit_deletion(&panel, cx);
4847    assert_eq!(
4848        visible_entries_as_strings(&panel, 0..15, cx),
4849        &[
4850            "v root",
4851            "    v dir1",
4852            "        > subdir1  <== selected",
4853            "    v dir2",
4854            "        > subdir2",
4855            "          file3.txt",
4856            "          file4.txt",
4857            "      file5.txt",
4858            "      file6.txt",
4859        ],
4860        "Should select next directory when last file is deleted"
4861    );
4862
4863    // Test Case 3: Delete root level file
4864    select_path(&panel, "root/file6.txt", cx);
4865    assert_eq!(
4866        visible_entries_as_strings(&panel, 0..15, cx),
4867        &[
4868            "v root",
4869            "    v dir1",
4870            "        > subdir1",
4871            "    v dir2",
4872            "        > subdir2",
4873            "          file3.txt",
4874            "          file4.txt",
4875            "      file5.txt",
4876            "      file6.txt  <== selected",
4877        ],
4878        "Initial state before deleting root level file"
4879    );
4880
4881    submit_deletion(&panel, cx);
4882    assert_eq!(
4883        visible_entries_as_strings(&panel, 0..15, cx),
4884        &[
4885            "v root",
4886            "    v dir1",
4887            "        > subdir1",
4888            "    v dir2",
4889            "        > subdir2",
4890            "          file3.txt",
4891            "          file4.txt",
4892            "      file5.txt  <== selected",
4893        ],
4894        "Should select prev entry at root level"
4895    );
4896}
4897
4898#[gpui::test]
4899async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
4900    init_test_with_editor(cx);
4901
4902    let fs = FakeFs::new(cx.executor());
4903    fs.insert_tree(
4904        path!("/root"),
4905        json!({
4906            "aa": "// Testing 1",
4907            "bb": "// Testing 2",
4908            "cc": "// Testing 3",
4909            "dd": "// Testing 4",
4910            "ee": "// Testing 5",
4911            "ff": "// Testing 6",
4912            "gg": "// Testing 7",
4913            "hh": "// Testing 8",
4914            "ii": "// Testing 8",
4915            ".gitignore": "bb\ndd\nee\nff\nii\n'",
4916        }),
4917    )
4918    .await;
4919
4920    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4921    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4922    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4923
4924    // Test 1: Auto selection with one gitignored file next to the deleted file
4925    cx.update(|_, cx| {
4926        let settings = *ProjectPanelSettings::get_global(cx);
4927        ProjectPanelSettings::override_global(
4928            ProjectPanelSettings {
4929                hide_gitignore: true,
4930                ..settings
4931            },
4932            cx,
4933        );
4934    });
4935
4936    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4937    cx.run_until_parked();
4938
4939    select_path(&panel, "root/aa", cx);
4940    assert_eq!(
4941        visible_entries_as_strings(&panel, 0..10, cx),
4942        &[
4943            "v root",
4944            "      .gitignore",
4945            "      aa  <== selected",
4946            "      cc",
4947            "      gg",
4948            "      hh"
4949        ],
4950        "Initial state should hide files on .gitignore"
4951    );
4952
4953    submit_deletion(&panel, cx);
4954
4955    assert_eq!(
4956        visible_entries_as_strings(&panel, 0..10, cx),
4957        &[
4958            "v root",
4959            "      .gitignore",
4960            "      cc  <== selected",
4961            "      gg",
4962            "      hh"
4963        ],
4964        "Should select next entry not on .gitignore"
4965    );
4966
4967    // Test 2: Auto selection with many gitignored files next to the deleted file
4968    submit_deletion(&panel, cx);
4969    assert_eq!(
4970        visible_entries_as_strings(&panel, 0..10, cx),
4971        &[
4972            "v root",
4973            "      .gitignore",
4974            "      gg  <== selected",
4975            "      hh"
4976        ],
4977        "Should select next entry not on .gitignore"
4978    );
4979
4980    // Test 3: Auto selection of entry before deleted file
4981    select_path(&panel, "root/hh", cx);
4982    assert_eq!(
4983        visible_entries_as_strings(&panel, 0..10, cx),
4984        &[
4985            "v root",
4986            "      .gitignore",
4987            "      gg",
4988            "      hh  <== selected"
4989        ],
4990        "Should select next entry not on .gitignore"
4991    );
4992    submit_deletion(&panel, cx);
4993    assert_eq!(
4994        visible_entries_as_strings(&panel, 0..10, cx),
4995        &["v root", "      .gitignore", "      gg  <== selected"],
4996        "Should select next entry not on .gitignore"
4997    );
4998}
4999
5000#[gpui::test]
5001async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
5002    init_test_with_editor(cx);
5003
5004    let fs = FakeFs::new(cx.executor());
5005    fs.insert_tree(
5006        path!("/root"),
5007        json!({
5008            "dir1": {
5009                "file1": "// Testing",
5010                "file2": "// Testing",
5011                "file3": "// Testing"
5012            },
5013            "aa": "// Testing",
5014            ".gitignore": "file1\nfile3\n",
5015        }),
5016    )
5017    .await;
5018
5019    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5020    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5021    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5022
5023    cx.update(|_, cx| {
5024        let settings = *ProjectPanelSettings::get_global(cx);
5025        ProjectPanelSettings::override_global(
5026            ProjectPanelSettings {
5027                hide_gitignore: true,
5028                ..settings
5029            },
5030            cx,
5031        );
5032    });
5033
5034    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5035    cx.run_until_parked();
5036
5037    // Test 1: Visible items should exclude files on gitignore
5038    toggle_expand_dir(&panel, "root/dir1", cx);
5039    select_path(&panel, "root/dir1/file2", cx);
5040    assert_eq!(
5041        visible_entries_as_strings(&panel, 0..10, cx),
5042        &[
5043            "v root",
5044            "    v dir1",
5045            "          file2  <== selected",
5046            "      .gitignore",
5047            "      aa"
5048        ],
5049        "Initial state should hide files on .gitignore"
5050    );
5051    submit_deletion(&panel, cx);
5052
5053    // Test 2: Auto selection should go to the parent
5054    assert_eq!(
5055        visible_entries_as_strings(&panel, 0..10, cx),
5056        &[
5057            "v root",
5058            "    v dir1  <== selected",
5059            "      .gitignore",
5060            "      aa"
5061        ],
5062        "Initial state should hide files on .gitignore"
5063    );
5064}
5065
5066#[gpui::test]
5067async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
5068    init_test_with_editor(cx);
5069
5070    let fs = FakeFs::new(cx.executor());
5071    fs.insert_tree(
5072        "/root",
5073        json!({
5074            "dir1": {
5075                "subdir1": {
5076                    "a.txt": "",
5077                    "b.txt": ""
5078                },
5079                "file1.txt": "",
5080            },
5081            "dir2": {
5082                "subdir2": {
5083                    "c.txt": "",
5084                    "d.txt": ""
5085                },
5086                "file2.txt": "",
5087            },
5088            "file3.txt": "",
5089        }),
5090    )
5091    .await;
5092
5093    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5094    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5095    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5096    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5097    cx.run_until_parked();
5098
5099    toggle_expand_dir(&panel, "root/dir1", cx);
5100    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5101    toggle_expand_dir(&panel, "root/dir2", cx);
5102    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5103
5104    // Test Case 1: Select and delete nested directory with parent
5105    cx.simulate_modifiers_change(gpui::Modifiers {
5106        control: true,
5107        ..Default::default()
5108    });
5109    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5110    select_path_with_mark(&panel, "root/dir1", cx);
5111
5112    assert_eq!(
5113        visible_entries_as_strings(&panel, 0..15, cx),
5114        &[
5115            "v root",
5116            "    v dir1  <== selected  <== marked",
5117            "        v subdir1  <== marked",
5118            "              a.txt",
5119            "              b.txt",
5120            "          file1.txt",
5121            "    v dir2",
5122            "        v subdir2",
5123            "              c.txt",
5124            "              d.txt",
5125            "          file2.txt",
5126            "      file3.txt",
5127        ],
5128        "Initial state before deleting nested directory with parent"
5129    );
5130
5131    submit_deletion(&panel, cx);
5132    assert_eq!(
5133        visible_entries_as_strings(&panel, 0..15, cx),
5134        &[
5135            "v root",
5136            "    v dir2  <== selected",
5137            "        v subdir2",
5138            "              c.txt",
5139            "              d.txt",
5140            "          file2.txt",
5141            "      file3.txt",
5142        ],
5143        "Should select next directory after deleting directory with parent"
5144    );
5145
5146    // Test Case 2: Select mixed files and directories across levels
5147    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
5148    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
5149    select_path_with_mark(&panel, "root/file3.txt", cx);
5150
5151    assert_eq!(
5152        visible_entries_as_strings(&panel, 0..15, cx),
5153        &[
5154            "v root",
5155            "    v dir2",
5156            "        v subdir2",
5157            "              c.txt  <== marked",
5158            "              d.txt",
5159            "          file2.txt  <== marked",
5160            "      file3.txt  <== selected  <== marked",
5161        ],
5162        "Initial state before deleting"
5163    );
5164
5165    submit_deletion(&panel, cx);
5166    assert_eq!(
5167        visible_entries_as_strings(&panel, 0..15, cx),
5168        &[
5169            "v root",
5170            "    v dir2  <== selected",
5171            "        v subdir2",
5172            "              d.txt",
5173        ],
5174        "Should select sibling directory"
5175    );
5176}
5177
5178#[gpui::test]
5179async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
5180    init_test_with_editor(cx);
5181
5182    let fs = FakeFs::new(cx.executor());
5183    fs.insert_tree(
5184        "/root",
5185        json!({
5186            "dir1": {
5187                "subdir1": {
5188                    "a.txt": "",
5189                    "b.txt": ""
5190                },
5191                "file1.txt": "",
5192            },
5193            "dir2": {
5194                "subdir2": {
5195                    "c.txt": "",
5196                    "d.txt": ""
5197                },
5198                "file2.txt": "",
5199            },
5200            "file3.txt": "",
5201            "file4.txt": "",
5202        }),
5203    )
5204    .await;
5205
5206    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5207    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5208    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5209    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5210    cx.run_until_parked();
5211
5212    toggle_expand_dir(&panel, "root/dir1", cx);
5213    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5214    toggle_expand_dir(&panel, "root/dir2", cx);
5215    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5216
5217    // Test Case 1: Select all root files and directories
5218    cx.simulate_modifiers_change(gpui::Modifiers {
5219        control: true,
5220        ..Default::default()
5221    });
5222    select_path_with_mark(&panel, "root/dir1", cx);
5223    select_path_with_mark(&panel, "root/dir2", cx);
5224    select_path_with_mark(&panel, "root/file3.txt", cx);
5225    select_path_with_mark(&panel, "root/file4.txt", cx);
5226    assert_eq!(
5227        visible_entries_as_strings(&panel, 0..20, cx),
5228        &[
5229            "v root",
5230            "    v dir1  <== marked",
5231            "        v subdir1",
5232            "              a.txt",
5233            "              b.txt",
5234            "          file1.txt",
5235            "    v dir2  <== marked",
5236            "        v subdir2",
5237            "              c.txt",
5238            "              d.txt",
5239            "          file2.txt",
5240            "      file3.txt  <== marked",
5241            "      file4.txt  <== selected  <== marked",
5242        ],
5243        "State before deleting all contents"
5244    );
5245
5246    submit_deletion(&panel, cx);
5247    assert_eq!(
5248        visible_entries_as_strings(&panel, 0..20, cx),
5249        &["v root  <== selected"],
5250        "Only empty root directory should remain after deleting all contents"
5251    );
5252}
5253
5254#[gpui::test]
5255async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
5256    init_test_with_editor(cx);
5257
5258    let fs = FakeFs::new(cx.executor());
5259    fs.insert_tree(
5260        "/root",
5261        json!({
5262            "dir1": {
5263                "subdir1": {
5264                    "file_a.txt": "content a",
5265                    "file_b.txt": "content b",
5266                },
5267                "subdir2": {
5268                    "file_c.txt": "content c",
5269                },
5270                "file1.txt": "content 1",
5271            },
5272            "dir2": {
5273                "file2.txt": "content 2",
5274            },
5275        }),
5276    )
5277    .await;
5278
5279    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5280    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5281    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5282    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5283    cx.run_until_parked();
5284
5285    toggle_expand_dir(&panel, "root/dir1", cx);
5286    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5287    toggle_expand_dir(&panel, "root/dir2", cx);
5288    cx.simulate_modifiers_change(gpui::Modifiers {
5289        control: true,
5290        ..Default::default()
5291    });
5292
5293    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
5294    select_path_with_mark(&panel, "root/dir1", cx);
5295    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5296    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
5297
5298    assert_eq!(
5299        visible_entries_as_strings(&panel, 0..20, cx),
5300        &[
5301            "v root",
5302            "    v dir1  <== marked",
5303            "        v subdir1  <== marked",
5304            "              file_a.txt  <== selected  <== marked",
5305            "              file_b.txt",
5306            "        > subdir2",
5307            "          file1.txt",
5308            "    v dir2",
5309            "          file2.txt",
5310        ],
5311        "State with parent dir, subdir, and file selected"
5312    );
5313    submit_deletion(&panel, cx);
5314    assert_eq!(
5315        visible_entries_as_strings(&panel, 0..20, cx),
5316        &["v root", "    v dir2  <== selected", "          file2.txt",],
5317        "Only dir2 should remain after deletion"
5318    );
5319}
5320
5321#[gpui::test]
5322async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
5323    init_test_with_editor(cx);
5324
5325    let fs = FakeFs::new(cx.executor());
5326    // First worktree
5327    fs.insert_tree(
5328        "/root1",
5329        json!({
5330            "dir1": {
5331                "file1.txt": "content 1",
5332                "file2.txt": "content 2",
5333            },
5334            "dir2": {
5335                "file3.txt": "content 3",
5336            },
5337        }),
5338    )
5339    .await;
5340
5341    // Second worktree
5342    fs.insert_tree(
5343        "/root2",
5344        json!({
5345            "dir3": {
5346                "file4.txt": "content 4",
5347                "file5.txt": "content 5",
5348            },
5349            "file6.txt": "content 6",
5350        }),
5351    )
5352    .await;
5353
5354    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5355    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5356    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5357    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5358    cx.run_until_parked();
5359
5360    // Expand all directories for testing
5361    toggle_expand_dir(&panel, "root1/dir1", cx);
5362    toggle_expand_dir(&panel, "root1/dir2", cx);
5363    toggle_expand_dir(&panel, "root2/dir3", cx);
5364
5365    // Test Case 1: Delete files across different worktrees
5366    cx.simulate_modifiers_change(gpui::Modifiers {
5367        control: true,
5368        ..Default::default()
5369    });
5370    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
5371    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
5372
5373    assert_eq!(
5374        visible_entries_as_strings(&panel, 0..20, cx),
5375        &[
5376            "v root1",
5377            "    v dir1",
5378            "          file1.txt  <== marked",
5379            "          file2.txt",
5380            "    v dir2",
5381            "          file3.txt",
5382            "v root2",
5383            "    v dir3",
5384            "          file4.txt  <== selected  <== marked",
5385            "          file5.txt",
5386            "      file6.txt",
5387        ],
5388        "Initial state with files selected from different worktrees"
5389    );
5390
5391    submit_deletion(&panel, cx);
5392    assert_eq!(
5393        visible_entries_as_strings(&panel, 0..20, cx),
5394        &[
5395            "v root1",
5396            "    v dir1",
5397            "          file2.txt",
5398            "    v dir2",
5399            "          file3.txt",
5400            "v root2",
5401            "    v dir3",
5402            "          file5.txt  <== selected",
5403            "      file6.txt",
5404        ],
5405        "Should select next file in the last worktree after deletion"
5406    );
5407
5408    // Test Case 2: Delete directories from different worktrees
5409    select_path_with_mark(&panel, "root1/dir1", cx);
5410    select_path_with_mark(&panel, "root2/dir3", cx);
5411
5412    assert_eq!(
5413        visible_entries_as_strings(&panel, 0..20, cx),
5414        &[
5415            "v root1",
5416            "    v dir1  <== marked",
5417            "          file2.txt",
5418            "    v dir2",
5419            "          file3.txt",
5420            "v root2",
5421            "    v dir3  <== selected  <== marked",
5422            "          file5.txt",
5423            "      file6.txt",
5424        ],
5425        "State with directories marked from different worktrees"
5426    );
5427
5428    submit_deletion(&panel, cx);
5429    assert_eq!(
5430        visible_entries_as_strings(&panel, 0..20, cx),
5431        &[
5432            "v root1",
5433            "    v dir2",
5434            "          file3.txt",
5435            "v root2",
5436            "      file6.txt  <== selected",
5437        ],
5438        "Should select remaining file in last worktree after directory deletion"
5439    );
5440
5441    // Test Case 4: Delete all remaining files except roots
5442    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
5443    select_path_with_mark(&panel, "root2/file6.txt", cx);
5444
5445    assert_eq!(
5446        visible_entries_as_strings(&panel, 0..20, cx),
5447        &[
5448            "v root1",
5449            "    v dir2",
5450            "          file3.txt  <== marked",
5451            "v root2",
5452            "      file6.txt  <== selected  <== marked",
5453        ],
5454        "State with all remaining files marked"
5455    );
5456
5457    submit_deletion(&panel, cx);
5458    assert_eq!(
5459        visible_entries_as_strings(&panel, 0..20, cx),
5460        &["v root1", "    v dir2", "v root2  <== selected"],
5461        "Second parent root should be selected after deleting"
5462    );
5463}
5464
5465#[gpui::test]
5466async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
5467    init_test_with_editor(cx);
5468
5469    let fs = FakeFs::new(cx.executor());
5470    fs.insert_tree(
5471        "/root",
5472        json!({
5473            "dir1": {
5474                "file1.txt": "",
5475                "file2.txt": "",
5476                "file3.txt": "",
5477            },
5478            "dir2": {
5479                "file4.txt": "",
5480                "file5.txt": "",
5481            },
5482        }),
5483    )
5484    .await;
5485
5486    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5487    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5488    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5489    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5490    cx.run_until_parked();
5491
5492    toggle_expand_dir(&panel, "root/dir1", cx);
5493    toggle_expand_dir(&panel, "root/dir2", cx);
5494
5495    cx.simulate_modifiers_change(gpui::Modifiers {
5496        control: true,
5497        ..Default::default()
5498    });
5499
5500    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
5501    select_path(&panel, "root/dir1/file1.txt", cx);
5502
5503    assert_eq!(
5504        visible_entries_as_strings(&panel, 0..15, cx),
5505        &[
5506            "v root",
5507            "    v dir1",
5508            "          file1.txt  <== selected",
5509            "          file2.txt  <== marked",
5510            "          file3.txt",
5511            "    v dir2",
5512            "          file4.txt",
5513            "          file5.txt",
5514        ],
5515        "Initial state with one marked entry and different selection"
5516    );
5517
5518    // Delete should operate on the selected entry (file1.txt)
5519    submit_deletion(&panel, cx);
5520    assert_eq!(
5521        visible_entries_as_strings(&panel, 0..15, cx),
5522        &[
5523            "v root",
5524            "    v dir1",
5525            "          file2.txt  <== selected  <== marked",
5526            "          file3.txt",
5527            "    v dir2",
5528            "          file4.txt",
5529            "          file5.txt",
5530        ],
5531        "Should delete selected file, not marked file"
5532    );
5533
5534    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
5535    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
5536    select_path(&panel, "root/dir2/file5.txt", cx);
5537
5538    assert_eq!(
5539        visible_entries_as_strings(&panel, 0..15, cx),
5540        &[
5541            "v root",
5542            "    v dir1",
5543            "          file2.txt  <== marked",
5544            "          file3.txt  <== marked",
5545            "    v dir2",
5546            "          file4.txt  <== marked",
5547            "          file5.txt  <== selected",
5548        ],
5549        "Initial state with multiple marked entries and different selection"
5550    );
5551
5552    // Delete should operate on all marked entries, ignoring the selection
5553    submit_deletion(&panel, cx);
5554    assert_eq!(
5555        visible_entries_as_strings(&panel, 0..15, cx),
5556        &[
5557            "v root",
5558            "    v dir1",
5559            "    v dir2",
5560            "          file5.txt  <== selected",
5561        ],
5562        "Should delete all marked files, leaving only the selected file"
5563    );
5564}
5565
5566#[gpui::test]
5567async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
5568    init_test_with_editor(cx);
5569
5570    let fs = FakeFs::new(cx.executor());
5571    fs.insert_tree(
5572        "/root_b",
5573        json!({
5574            "dir1": {
5575                "file1.txt": "content 1",
5576                "file2.txt": "content 2",
5577            },
5578        }),
5579    )
5580    .await;
5581
5582    fs.insert_tree(
5583        "/root_c",
5584        json!({
5585            "dir2": {},
5586        }),
5587    )
5588    .await;
5589
5590    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
5591    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5592    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5593    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5594    cx.run_until_parked();
5595
5596    toggle_expand_dir(&panel, "root_b/dir1", cx);
5597    toggle_expand_dir(&panel, "root_c/dir2", cx);
5598
5599    cx.simulate_modifiers_change(gpui::Modifiers {
5600        control: true,
5601        ..Default::default()
5602    });
5603    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
5604    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
5605
5606    assert_eq!(
5607        visible_entries_as_strings(&panel, 0..20, cx),
5608        &[
5609            "v root_b",
5610            "    v dir1",
5611            "          file1.txt  <== marked",
5612            "          file2.txt  <== selected  <== marked",
5613            "v root_c",
5614            "    v dir2",
5615        ],
5616        "Initial state with files marked in root_b"
5617    );
5618
5619    submit_deletion(&panel, cx);
5620    assert_eq!(
5621        visible_entries_as_strings(&panel, 0..20, cx),
5622        &[
5623            "v root_b",
5624            "    v dir1  <== selected",
5625            "v root_c",
5626            "    v dir2",
5627        ],
5628        "After deletion in root_b as it's last deletion, selection should be in root_b"
5629    );
5630
5631    select_path_with_mark(&panel, "root_c/dir2", cx);
5632
5633    submit_deletion(&panel, cx);
5634    assert_eq!(
5635        visible_entries_as_strings(&panel, 0..20, cx),
5636        &["v root_b", "    v dir1", "v root_c  <== selected",],
5637        "After deleting from root_c, it should remain in root_c"
5638    );
5639}
5640
5641fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
5642    let path = rel_path(path);
5643    panel.update_in(cx, |panel, window, cx| {
5644        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5645            let worktree = worktree.read(cx);
5646            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5647                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5648                panel.toggle_expanded(entry_id, window, cx);
5649                return;
5650            }
5651        }
5652        panic!("no worktree for path {:?}", path);
5653    });
5654    cx.run_until_parked();
5655}
5656
5657#[gpui::test]
5658async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
5659    init_test_with_editor(cx);
5660
5661    let fs = FakeFs::new(cx.executor());
5662    fs.insert_tree(
5663        path!("/root"),
5664        json!({
5665            ".gitignore": "**/ignored_dir\n**/ignored_nested",
5666            "dir1": {
5667                "empty1": {
5668                    "empty2": {
5669                        "empty3": {
5670                            "file.txt": ""
5671                        }
5672                    }
5673                },
5674                "subdir1": {
5675                    "file1.txt": "",
5676                    "file2.txt": "",
5677                    "ignored_nested": {
5678                        "ignored_file.txt": ""
5679                    }
5680                },
5681                "ignored_dir": {
5682                    "subdir": {
5683                        "deep_file.txt": ""
5684                    }
5685                }
5686            }
5687        }),
5688    )
5689    .await;
5690
5691    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5692    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5693    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5694
5695    // Test 1: When auto-fold is enabled
5696    cx.update(|_, cx| {
5697        let settings = *ProjectPanelSettings::get_global(cx);
5698        ProjectPanelSettings::override_global(
5699            ProjectPanelSettings {
5700                auto_fold_dirs: true,
5701                ..settings
5702            },
5703            cx,
5704        );
5705    });
5706
5707    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5708    cx.run_until_parked();
5709
5710    assert_eq!(
5711        visible_entries_as_strings(&panel, 0..20, cx),
5712        &["v root", "    > dir1", "      .gitignore",],
5713        "Initial state should show collapsed root structure"
5714    );
5715
5716    toggle_expand_dir(&panel, "root/dir1", cx);
5717    assert_eq!(
5718        visible_entries_as_strings(&panel, 0..20, cx),
5719        &[
5720            "v root",
5721            "    v dir1  <== selected",
5722            "        > empty1/empty2/empty3",
5723            "        > ignored_dir",
5724            "        > subdir1",
5725            "      .gitignore",
5726        ],
5727        "Should show first level with auto-folded dirs and ignored dir visible"
5728    );
5729
5730    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5731    panel.update_in(cx, |panel, window, cx| {
5732        let project = panel.project.read(cx);
5733        let worktree = project.worktrees(cx).next().unwrap().read(cx);
5734        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
5735        panel.update_visible_entries(None, false, false, window, cx);
5736    });
5737    cx.run_until_parked();
5738
5739    assert_eq!(
5740        visible_entries_as_strings(&panel, 0..20, cx),
5741        &[
5742            "v root",
5743            "    v dir1  <== selected",
5744            "        v empty1",
5745            "            v empty2",
5746            "                v empty3",
5747            "                      file.txt",
5748            "        > ignored_dir",
5749            "        v subdir1",
5750            "            > ignored_nested",
5751            "              file1.txt",
5752            "              file2.txt",
5753            "      .gitignore",
5754        ],
5755        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
5756    );
5757
5758    // Test 2: When auto-fold is disabled
5759    cx.update(|_, cx| {
5760        let settings = *ProjectPanelSettings::get_global(cx);
5761        ProjectPanelSettings::override_global(
5762            ProjectPanelSettings {
5763                auto_fold_dirs: false,
5764                ..settings
5765            },
5766            cx,
5767        );
5768    });
5769
5770    panel.update_in(cx, |panel, window, cx| {
5771        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
5772    });
5773
5774    toggle_expand_dir(&panel, "root/dir1", cx);
5775    assert_eq!(
5776        visible_entries_as_strings(&panel, 0..20, cx),
5777        &[
5778            "v root",
5779            "    v dir1  <== selected",
5780            "        > empty1",
5781            "        > ignored_dir",
5782            "        > subdir1",
5783            "      .gitignore",
5784        ],
5785        "With auto-fold disabled: should show all directories separately"
5786    );
5787
5788    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5789    panel.update_in(cx, |panel, window, cx| {
5790        let project = panel.project.read(cx);
5791        let worktree = project.worktrees(cx).next().unwrap().read(cx);
5792        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
5793        panel.update_visible_entries(None, false, false, window, cx);
5794    });
5795    cx.run_until_parked();
5796
5797    assert_eq!(
5798        visible_entries_as_strings(&panel, 0..20, cx),
5799        &[
5800            "v root",
5801            "    v dir1  <== selected",
5802            "        v empty1",
5803            "            v empty2",
5804            "                v empty3",
5805            "                      file.txt",
5806            "        > ignored_dir",
5807            "        v subdir1",
5808            "            > ignored_nested",
5809            "              file1.txt",
5810            "              file2.txt",
5811            "      .gitignore",
5812        ],
5813        "After expand_all without auto-fold: should expand all dirs normally, \
5814         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
5815    );
5816
5817    // Test 3: When explicitly called on ignored directory
5818    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
5819    panel.update_in(cx, |panel, window, cx| {
5820        let project = panel.project.read(cx);
5821        let worktree = project.worktrees(cx).next().unwrap().read(cx);
5822        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
5823        panel.update_visible_entries(None, false, false, window, cx);
5824    });
5825    cx.run_until_parked();
5826
5827    assert_eq!(
5828        visible_entries_as_strings(&panel, 0..20, cx),
5829        &[
5830            "v root",
5831            "    v dir1  <== selected",
5832            "        v empty1",
5833            "            v empty2",
5834            "                v empty3",
5835            "                      file.txt",
5836            "        v ignored_dir",
5837            "            v subdir",
5838            "                  deep_file.txt",
5839            "        v subdir1",
5840            "            > ignored_nested",
5841            "              file1.txt",
5842            "              file2.txt",
5843            "      .gitignore",
5844        ],
5845        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
5846    );
5847}
5848
5849#[gpui::test]
5850async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
5851    init_test(cx);
5852
5853    let fs = FakeFs::new(cx.executor());
5854    fs.insert_tree(
5855        path!("/root"),
5856        json!({
5857            "dir1": {
5858                "subdir1": {
5859                    "nested1": {
5860                        "file1.txt": "",
5861                        "file2.txt": ""
5862                    },
5863                },
5864                "subdir2": {
5865                    "file4.txt": ""
5866                }
5867            },
5868            "dir2": {
5869                "single_file": {
5870                    "file5.txt": ""
5871                }
5872            }
5873        }),
5874    )
5875    .await;
5876
5877    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5878    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5879    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5880
5881    // Test 1: Basic collapsing
5882    {
5883        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5884        cx.run_until_parked();
5885
5886        toggle_expand_dir(&panel, "root/dir1", cx);
5887        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5888        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5889        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
5890
5891        assert_eq!(
5892            visible_entries_as_strings(&panel, 0..20, cx),
5893            &[
5894                "v root",
5895                "    v dir1",
5896                "        v subdir1",
5897                "            v nested1",
5898                "                  file1.txt",
5899                "                  file2.txt",
5900                "        v subdir2  <== selected",
5901                "              file4.txt",
5902                "    > dir2",
5903            ],
5904            "Initial state with everything expanded"
5905        );
5906
5907        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5908        panel.update_in(cx, |panel, window, cx| {
5909            let project = panel.project.read(cx);
5910            let worktree = project.worktrees(cx).next().unwrap().read(cx);
5911            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5912            panel.update_visible_entries(None, false, false, window, cx);
5913        });
5914        cx.run_until_parked();
5915
5916        assert_eq!(
5917            visible_entries_as_strings(&panel, 0..20, cx),
5918            &["v root", "    > dir1", "    > dir2",],
5919            "All subdirs under dir1 should be collapsed"
5920        );
5921    }
5922
5923    // Test 2: With auto-fold enabled
5924    {
5925        cx.update(|_, cx| {
5926            let settings = *ProjectPanelSettings::get_global(cx);
5927            ProjectPanelSettings::override_global(
5928                ProjectPanelSettings {
5929                    auto_fold_dirs: true,
5930                    ..settings
5931                },
5932                cx,
5933            );
5934        });
5935
5936        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5937        cx.run_until_parked();
5938
5939        toggle_expand_dir(&panel, "root/dir1", cx);
5940        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5941        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5942
5943        assert_eq!(
5944            visible_entries_as_strings(&panel, 0..20, cx),
5945            &[
5946                "v root",
5947                "    v dir1",
5948                "        v subdir1/nested1  <== selected",
5949                "              file1.txt",
5950                "              file2.txt",
5951                "        > subdir2",
5952                "    > dir2/single_file",
5953            ],
5954            "Initial state with some dirs expanded"
5955        );
5956
5957        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5958        panel.update(cx, |panel, cx| {
5959            let project = panel.project.read(cx);
5960            let worktree = project.worktrees(cx).next().unwrap().read(cx);
5961            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5962        });
5963
5964        toggle_expand_dir(&panel, "root/dir1", cx);
5965
5966        assert_eq!(
5967            visible_entries_as_strings(&panel, 0..20, cx),
5968            &[
5969                "v root",
5970                "    v dir1  <== selected",
5971                "        > subdir1/nested1",
5972                "        > subdir2",
5973                "    > dir2/single_file",
5974            ],
5975            "Subdirs should be collapsed and folded with auto-fold enabled"
5976        );
5977    }
5978
5979    // Test 3: With auto-fold disabled
5980    {
5981        cx.update(|_, cx| {
5982            let settings = *ProjectPanelSettings::get_global(cx);
5983            ProjectPanelSettings::override_global(
5984                ProjectPanelSettings {
5985                    auto_fold_dirs: false,
5986                    ..settings
5987                },
5988                cx,
5989            );
5990        });
5991
5992        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5993        cx.run_until_parked();
5994
5995        toggle_expand_dir(&panel, "root/dir1", cx);
5996        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5997        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5998
5999        assert_eq!(
6000            visible_entries_as_strings(&panel, 0..20, cx),
6001            &[
6002                "v root",
6003                "    v dir1",
6004                "        v subdir1",
6005                "            v nested1  <== selected",
6006                "                  file1.txt",
6007                "                  file2.txt",
6008                "        > subdir2",
6009                "    > dir2",
6010            ],
6011            "Initial state with some dirs expanded and auto-fold disabled"
6012        );
6013
6014        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6015        panel.update(cx, |panel, cx| {
6016            let project = panel.project.read(cx);
6017            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6018            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6019        });
6020
6021        toggle_expand_dir(&panel, "root/dir1", cx);
6022
6023        assert_eq!(
6024            visible_entries_as_strings(&panel, 0..20, cx),
6025            &[
6026                "v root",
6027                "    v dir1  <== selected",
6028                "        > subdir1",
6029                "        > subdir2",
6030                "    > dir2",
6031            ],
6032            "Subdirs should be collapsed but not folded with auto-fold disabled"
6033        );
6034    }
6035}
6036
6037#[gpui::test]
6038async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
6039    init_test(cx);
6040
6041    let fs = FakeFs::new(cx.executor());
6042    fs.insert_tree(
6043        path!("/root"),
6044        json!({
6045            "dir1": {
6046                "file1.txt": "",
6047            },
6048        }),
6049    )
6050    .await;
6051
6052    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6053    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6054    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6055
6056    let panel = workspace
6057        .update(cx, |workspace, window, cx| {
6058            let panel = ProjectPanel::new(workspace, window, cx);
6059            workspace.add_panel(panel.clone(), window, cx);
6060            panel
6061        })
6062        .unwrap();
6063    cx.run_until_parked();
6064
6065    #[rustfmt::skip]
6066    assert_eq!(
6067        visible_entries_as_strings(&panel, 0..20, cx),
6068        &[
6069            "v root",
6070            "    > dir1",
6071        ],
6072        "Initial state with nothing selected"
6073    );
6074
6075    panel.update_in(cx, |panel, window, cx| {
6076        panel.new_file(&NewFile, window, cx);
6077    });
6078    cx.run_until_parked();
6079    panel.update_in(cx, |panel, window, cx| {
6080        assert!(panel.filename_editor.read(cx).is_focused(window));
6081    });
6082    panel
6083        .update_in(cx, |panel, window, cx| {
6084            panel.filename_editor.update(cx, |editor, cx| {
6085                editor.set_text("hello_from_no_selections", window, cx)
6086            });
6087            panel.confirm_edit(true, window, cx).unwrap()
6088        })
6089        .await
6090        .unwrap();
6091    cx.run_until_parked();
6092    #[rustfmt::skip]
6093    assert_eq!(
6094        visible_entries_as_strings(&panel, 0..20, cx),
6095        &[
6096            "v root",
6097            "    > dir1",
6098            "      hello_from_no_selections  <== selected  <== marked",
6099        ],
6100        "A new file is created under the root directory"
6101    );
6102}
6103
6104#[gpui::test]
6105async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
6106    init_test(cx);
6107
6108    let fs = FakeFs::new(cx.executor());
6109    fs.insert_tree(
6110        path!("/root"),
6111        json!({
6112            "existing_dir": {
6113                "existing_file.txt": "",
6114            },
6115            "existing_file.txt": "",
6116        }),
6117    )
6118    .await;
6119
6120    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6121    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6122    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6123
6124    cx.update(|_, cx| {
6125        let settings = *ProjectPanelSettings::get_global(cx);
6126        ProjectPanelSettings::override_global(
6127            ProjectPanelSettings {
6128                hide_root: true,
6129                ..settings
6130            },
6131            cx,
6132        );
6133    });
6134
6135    let panel = workspace
6136        .update(cx, |workspace, window, cx| {
6137            let panel = ProjectPanel::new(workspace, window, cx);
6138            workspace.add_panel(panel.clone(), window, cx);
6139            panel
6140        })
6141        .unwrap();
6142    cx.run_until_parked();
6143
6144    #[rustfmt::skip]
6145    assert_eq!(
6146        visible_entries_as_strings(&panel, 0..20, cx),
6147        &[
6148            "> existing_dir",
6149            "  existing_file.txt",
6150        ],
6151        "Initial state with hide_root=true, root should be hidden and nothing selected"
6152    );
6153
6154    panel.update(cx, |panel, _| {
6155        assert!(
6156            panel.state.selection.is_none(),
6157            "Should have no selection initially"
6158        );
6159    });
6160
6161    // Test 1: Create new file when no entry is selected
6162    panel.update_in(cx, |panel, window, cx| {
6163        panel.new_file(&NewFile, window, cx);
6164    });
6165    cx.run_until_parked();
6166    panel.update_in(cx, |panel, window, cx| {
6167        assert!(panel.filename_editor.read(cx).is_focused(window));
6168    });
6169    cx.run_until_parked();
6170    #[rustfmt::skip]
6171    assert_eq!(
6172        visible_entries_as_strings(&panel, 0..20, cx),
6173        &[
6174            "> existing_dir",
6175            "  [EDITOR: '']  <== selected",
6176            "  existing_file.txt",
6177        ],
6178        "Editor should appear at root level when hide_root=true and no selection"
6179    );
6180
6181    let confirm = panel.update_in(cx, |panel, window, cx| {
6182        panel.filename_editor.update(cx, |editor, cx| {
6183            editor.set_text("new_file_at_root.txt", window, cx)
6184        });
6185        panel.confirm_edit(true, window, cx).unwrap()
6186    });
6187    confirm.await.unwrap();
6188    cx.run_until_parked();
6189
6190    #[rustfmt::skip]
6191    assert_eq!(
6192        visible_entries_as_strings(&panel, 0..20, cx),
6193        &[
6194            "> existing_dir",
6195            "  existing_file.txt",
6196            "  new_file_at_root.txt  <== selected  <== marked",
6197        ],
6198        "New file should be created at root level and visible without root prefix"
6199    );
6200
6201    assert!(
6202        fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
6203        "File should be created in the actual root directory"
6204    );
6205
6206    // Test 2: Create new directory when no entry is selected
6207    panel.update(cx, |panel, _| {
6208        panel.state.selection = None;
6209    });
6210
6211    panel.update_in(cx, |panel, window, cx| {
6212        panel.new_directory(&NewDirectory, window, cx);
6213    });
6214    cx.run_until_parked();
6215
6216    panel.update_in(cx, |panel, window, cx| {
6217        assert!(panel.filename_editor.read(cx).is_focused(window));
6218    });
6219
6220    #[rustfmt::skip]
6221    assert_eq!(
6222        visible_entries_as_strings(&panel, 0..20, cx),
6223        &[
6224            "> [EDITOR: '']  <== selected",
6225            "> existing_dir",
6226            "  existing_file.txt",
6227            "  new_file_at_root.txt",
6228        ],
6229        "Directory editor should appear at root level when hide_root=true and no selection"
6230    );
6231
6232    let confirm = panel.update_in(cx, |panel, window, cx| {
6233        panel.filename_editor.update(cx, |editor, cx| {
6234            editor.set_text("new_dir_at_root", window, cx)
6235        });
6236        panel.confirm_edit(true, window, cx).unwrap()
6237    });
6238    confirm.await.unwrap();
6239    cx.run_until_parked();
6240
6241    #[rustfmt::skip]
6242    assert_eq!(
6243        visible_entries_as_strings(&panel, 0..20, cx),
6244        &[
6245            "> existing_dir",
6246            "v new_dir_at_root  <== selected",
6247            "  existing_file.txt",
6248            "  new_file_at_root.txt",
6249        ],
6250        "New directory should be created at root level and visible without root prefix"
6251    );
6252
6253    assert!(
6254        fs.is_dir(Path::new("/root/new_dir_at_root")).await,
6255        "Directory should be created in the actual root directory"
6256    );
6257}
6258
6259#[gpui::test]
6260async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
6261    init_test(cx);
6262
6263    let fs = FakeFs::new(cx.executor());
6264    fs.insert_tree(
6265        "/root",
6266        json!({
6267            "dir1": {
6268                "file1.txt": "",
6269                "dir2": {
6270                    "file2.txt": ""
6271                }
6272            },
6273            "file3.txt": ""
6274        }),
6275    )
6276    .await;
6277
6278    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6279    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6280    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6281    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6282    cx.run_until_parked();
6283
6284    panel.update(cx, |panel, cx| {
6285        let project = panel.project.read(cx);
6286        let worktree = project.visible_worktrees(cx).next().unwrap();
6287        let worktree = worktree.read(cx);
6288
6289        // Test 1: Target is a directory, should highlight the directory itself
6290        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
6291        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
6292        assert_eq!(
6293            result,
6294            Some(dir_entry.id),
6295            "Should highlight directory itself"
6296        );
6297
6298        // Test 2: Target is nested file, should highlight immediate parent
6299        let nested_file = worktree
6300            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
6301            .unwrap();
6302        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
6303        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
6304        assert_eq!(
6305            result,
6306            Some(nested_parent.id),
6307            "Should highlight immediate parent"
6308        );
6309
6310        // Test 3: Target is root level file, should highlight root
6311        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
6312        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
6313        assert_eq!(
6314            result,
6315            Some(worktree.root_entry().unwrap().id),
6316            "Root level file should return None"
6317        );
6318
6319        // Test 4: Target is root itself, should highlight root
6320        let root_entry = worktree.root_entry().unwrap();
6321        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
6322        assert_eq!(
6323            result,
6324            Some(root_entry.id),
6325            "Root level file should return None"
6326        );
6327    });
6328}
6329
6330#[gpui::test]
6331async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
6332    init_test(cx);
6333
6334    let fs = FakeFs::new(cx.executor());
6335    fs.insert_tree(
6336        "/root",
6337        json!({
6338            "parent_dir": {
6339                "child_file.txt": "",
6340                "sibling_file.txt": "",
6341                "child_dir": {
6342                    "nested_file.txt": ""
6343                }
6344            },
6345            "other_dir": {
6346                "other_file.txt": ""
6347            }
6348        }),
6349    )
6350    .await;
6351
6352    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6353    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6354    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6355    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6356    cx.run_until_parked();
6357
6358    panel.update(cx, |panel, cx| {
6359        let project = panel.project.read(cx);
6360        let worktree = project.visible_worktrees(cx).next().unwrap();
6361        let worktree_id = worktree.read(cx).id();
6362        let worktree = worktree.read(cx);
6363
6364        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
6365        let child_file = worktree
6366            .entry_for_path(rel_path("parent_dir/child_file.txt"))
6367            .unwrap();
6368        let sibling_file = worktree
6369            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
6370            .unwrap();
6371        let child_dir = worktree
6372            .entry_for_path(rel_path("parent_dir/child_dir"))
6373            .unwrap();
6374        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
6375        let other_file = worktree
6376            .entry_for_path(rel_path("other_dir/other_file.txt"))
6377            .unwrap();
6378
6379        // Test 1: Single item drag, don't highlight parent directory
6380        let dragged_selection = DraggedSelection {
6381            active_selection: SelectedEntry {
6382                worktree_id,
6383                entry_id: child_file.id,
6384            },
6385            marked_selections: Arc::new([SelectedEntry {
6386                worktree_id,
6387                entry_id: child_file.id,
6388            }]),
6389        };
6390        let result =
6391            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6392        assert_eq!(result, None, "Should not highlight parent of dragged item");
6393
6394        // Test 2: Single item drag, don't highlight sibling files
6395        let result = panel.highlight_entry_for_selection_drag(
6396            sibling_file,
6397            worktree,
6398            &dragged_selection,
6399            cx,
6400        );
6401        assert_eq!(result, None, "Should not highlight sibling files");
6402
6403        // Test 3: Single item drag, highlight unrelated directory
6404        let result =
6405            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
6406        assert_eq!(
6407            result,
6408            Some(other_dir.id),
6409            "Should highlight unrelated directory"
6410        );
6411
6412        // Test 4: Single item drag, highlight sibling directory
6413        let result =
6414            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6415        assert_eq!(
6416            result,
6417            Some(child_dir.id),
6418            "Should highlight sibling directory"
6419        );
6420
6421        // Test 5: Multiple items drag, highlight parent directory
6422        let dragged_selection = DraggedSelection {
6423            active_selection: SelectedEntry {
6424                worktree_id,
6425                entry_id: child_file.id,
6426            },
6427            marked_selections: Arc::new([
6428                SelectedEntry {
6429                    worktree_id,
6430                    entry_id: child_file.id,
6431                },
6432                SelectedEntry {
6433                    worktree_id,
6434                    entry_id: sibling_file.id,
6435                },
6436            ]),
6437        };
6438        let result =
6439            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6440        assert_eq!(
6441            result,
6442            Some(parent_dir.id),
6443            "Should highlight parent with multiple items"
6444        );
6445
6446        // Test 6: Target is file in different directory, highlight parent
6447        let result =
6448            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
6449        assert_eq!(
6450            result,
6451            Some(other_dir.id),
6452            "Should highlight parent of target file"
6453        );
6454
6455        // Test 7: Target is directory, always highlight
6456        let result =
6457            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6458        assert_eq!(
6459            result,
6460            Some(child_dir.id),
6461            "Should always highlight directories"
6462        );
6463    });
6464}
6465
6466#[gpui::test]
6467async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
6468    init_test(cx);
6469
6470    let fs = FakeFs::new(cx.executor());
6471    fs.insert_tree(
6472        "/root1",
6473        json!({
6474            "src": {
6475                "main.rs": "",
6476                "lib.rs": ""
6477            }
6478        }),
6479    )
6480    .await;
6481    fs.insert_tree(
6482        "/root2",
6483        json!({
6484            "src": {
6485                "main.rs": "",
6486                "test.rs": ""
6487            }
6488        }),
6489    )
6490    .await;
6491
6492    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6493    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6494    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6495    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6496    cx.run_until_parked();
6497
6498    panel.update(cx, |panel, cx| {
6499        let project = panel.project.read(cx);
6500        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6501
6502        let worktree_a = &worktrees[0];
6503        let main_rs_from_a = worktree_a
6504            .read(cx)
6505            .entry_for_path(rel_path("src/main.rs"))
6506            .unwrap();
6507
6508        let worktree_b = &worktrees[1];
6509        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
6510        let main_rs_from_b = worktree_b
6511            .read(cx)
6512            .entry_for_path(rel_path("src/main.rs"))
6513            .unwrap();
6514
6515        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
6516        let dragged_selection = DraggedSelection {
6517            active_selection: SelectedEntry {
6518                worktree_id: worktree_a.read(cx).id(),
6519                entry_id: main_rs_from_a.id,
6520            },
6521            marked_selections: Arc::new([SelectedEntry {
6522                worktree_id: worktree_a.read(cx).id(),
6523                entry_id: main_rs_from_a.id,
6524            }]),
6525        };
6526
6527        let result = panel.highlight_entry_for_selection_drag(
6528            src_dir_from_b,
6529            worktree_b.read(cx),
6530            &dragged_selection,
6531            cx,
6532        );
6533        assert_eq!(
6534            result,
6535            Some(src_dir_from_b.id),
6536            "Should highlight target directory from different worktree even with same relative path"
6537        );
6538
6539        // Test dragging file from worktree A onto file with same relative path in worktree B
6540        let result = panel.highlight_entry_for_selection_drag(
6541            main_rs_from_b,
6542            worktree_b.read(cx),
6543            &dragged_selection,
6544            cx,
6545        );
6546        assert_eq!(
6547            result,
6548            Some(src_dir_from_b.id),
6549            "Should highlight parent of target file from different worktree"
6550        );
6551    });
6552}
6553
6554#[gpui::test]
6555async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
6556    init_test(cx);
6557
6558    let fs = FakeFs::new(cx.executor());
6559    fs.insert_tree(
6560        "/root1",
6561        json!({
6562            "parent_dir": {
6563                "child_file.txt": "",
6564                "nested_dir": {
6565                    "nested_file.txt": ""
6566                }
6567            },
6568            "root_file.txt": ""
6569        }),
6570    )
6571    .await;
6572
6573    fs.insert_tree(
6574        "/root2",
6575        json!({
6576            "other_dir": {
6577                "other_file.txt": ""
6578            }
6579        }),
6580    )
6581    .await;
6582
6583    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6584    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6585    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6586    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6587    cx.run_until_parked();
6588
6589    panel.update(cx, |panel, cx| {
6590        let project = panel.project.read(cx);
6591        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6592        let worktree1 = worktrees[0].read(cx);
6593        let worktree2 = worktrees[1].read(cx);
6594        let worktree1_id = worktree1.id();
6595        let _worktree2_id = worktree2.id();
6596
6597        let root1_entry = worktree1.root_entry().unwrap();
6598        let root2_entry = worktree2.root_entry().unwrap();
6599        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
6600        let child_file = worktree1
6601            .entry_for_path(rel_path("parent_dir/child_file.txt"))
6602            .unwrap();
6603        let nested_file = worktree1
6604            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
6605            .unwrap();
6606        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
6607
6608        // Test 1: Multiple entries - should always highlight background
6609        let multiple_dragged_selection = DraggedSelection {
6610            active_selection: SelectedEntry {
6611                worktree_id: worktree1_id,
6612                entry_id: child_file.id,
6613            },
6614            marked_selections: Arc::new([
6615                SelectedEntry {
6616                    worktree_id: worktree1_id,
6617                    entry_id: child_file.id,
6618                },
6619                SelectedEntry {
6620                    worktree_id: worktree1_id,
6621                    entry_id: nested_file.id,
6622                },
6623            ]),
6624        };
6625
6626        let result = panel.should_highlight_background_for_selection_drag(
6627            &multiple_dragged_selection,
6628            root1_entry.id,
6629            cx,
6630        );
6631        assert!(result, "Should highlight background for multiple entries");
6632
6633        // Test 2: Single entry with non-empty parent path - should highlight background
6634        let nested_dragged_selection = DraggedSelection {
6635            active_selection: SelectedEntry {
6636                worktree_id: worktree1_id,
6637                entry_id: nested_file.id,
6638            },
6639            marked_selections: Arc::new([SelectedEntry {
6640                worktree_id: worktree1_id,
6641                entry_id: nested_file.id,
6642            }]),
6643        };
6644
6645        let result = panel.should_highlight_background_for_selection_drag(
6646            &nested_dragged_selection,
6647            root1_entry.id,
6648            cx,
6649        );
6650        assert!(result, "Should highlight background for nested file");
6651
6652        // Test 3: Single entry at root level, same worktree - should NOT highlight background
6653        let root_file_dragged_selection = DraggedSelection {
6654            active_selection: SelectedEntry {
6655                worktree_id: worktree1_id,
6656                entry_id: root_file.id,
6657            },
6658            marked_selections: Arc::new([SelectedEntry {
6659                worktree_id: worktree1_id,
6660                entry_id: root_file.id,
6661            }]),
6662        };
6663
6664        let result = panel.should_highlight_background_for_selection_drag(
6665            &root_file_dragged_selection,
6666            root1_entry.id,
6667            cx,
6668        );
6669        assert!(
6670            !result,
6671            "Should NOT highlight background for root file in same worktree"
6672        );
6673
6674        // Test 4: Single entry at root level, different worktree - should highlight background
6675        let result = panel.should_highlight_background_for_selection_drag(
6676            &root_file_dragged_selection,
6677            root2_entry.id,
6678            cx,
6679        );
6680        assert!(
6681            result,
6682            "Should highlight background for root file from different worktree"
6683        );
6684
6685        // Test 5: Single entry in subdirectory - should highlight background
6686        let child_file_dragged_selection = DraggedSelection {
6687            active_selection: SelectedEntry {
6688                worktree_id: worktree1_id,
6689                entry_id: child_file.id,
6690            },
6691            marked_selections: Arc::new([SelectedEntry {
6692                worktree_id: worktree1_id,
6693                entry_id: child_file.id,
6694            }]),
6695        };
6696
6697        let result = panel.should_highlight_background_for_selection_drag(
6698            &child_file_dragged_selection,
6699            root1_entry.id,
6700            cx,
6701        );
6702        assert!(
6703            result,
6704            "Should highlight background for file with non-empty parent path"
6705        );
6706    });
6707}
6708
6709#[gpui::test]
6710async fn test_hide_root(cx: &mut gpui::TestAppContext) {
6711    init_test(cx);
6712
6713    let fs = FakeFs::new(cx.executor());
6714    fs.insert_tree(
6715        "/root1",
6716        json!({
6717            "dir1": {
6718                "file1.txt": "content",
6719                "file2.txt": "content",
6720            },
6721            "dir2": {
6722                "file3.txt": "content",
6723            },
6724            "file4.txt": "content",
6725        }),
6726    )
6727    .await;
6728
6729    fs.insert_tree(
6730        "/root2",
6731        json!({
6732            "dir3": {
6733                "file5.txt": "content",
6734            },
6735            "file6.txt": "content",
6736        }),
6737    )
6738    .await;
6739
6740    // Test 1: Single worktree with hide_root = false
6741    {
6742        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
6743        let workspace =
6744            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6745        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6746
6747        cx.update(|_, cx| {
6748            let settings = *ProjectPanelSettings::get_global(cx);
6749            ProjectPanelSettings::override_global(
6750                ProjectPanelSettings {
6751                    hide_root: false,
6752                    ..settings
6753                },
6754                cx,
6755            );
6756        });
6757
6758        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6759        cx.run_until_parked();
6760
6761        #[rustfmt::skip]
6762        assert_eq!(
6763            visible_entries_as_strings(&panel, 0..10, cx),
6764            &[
6765                "v root1",
6766                "    > dir1",
6767                "    > dir2",
6768                "      file4.txt",
6769            ],
6770            "With hide_root=false and single worktree, root should be visible"
6771        );
6772    }
6773
6774    // Test 2: Single worktree with hide_root = true
6775    {
6776        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
6777        let workspace =
6778            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6779        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6780
6781        // Set hide_root to true
6782        cx.update(|_, cx| {
6783            let settings = *ProjectPanelSettings::get_global(cx);
6784            ProjectPanelSettings::override_global(
6785                ProjectPanelSettings {
6786                    hide_root: true,
6787                    ..settings
6788                },
6789                cx,
6790            );
6791        });
6792
6793        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6794        cx.run_until_parked();
6795
6796        assert_eq!(
6797            visible_entries_as_strings(&panel, 0..10, cx),
6798            &["> dir1", "> dir2", "  file4.txt",],
6799            "With hide_root=true and single worktree, root should be hidden"
6800        );
6801
6802        // Test expanding directories still works without root
6803        toggle_expand_dir(&panel, "root1/dir1", cx);
6804        assert_eq!(
6805            visible_entries_as_strings(&panel, 0..10, cx),
6806            &[
6807                "v dir1  <== selected",
6808                "      file1.txt",
6809                "      file2.txt",
6810                "> dir2",
6811                "  file4.txt",
6812            ],
6813            "Should be able to expand directories even when root is hidden"
6814        );
6815    }
6816
6817    // Test 3: Multiple worktrees with hide_root = true
6818    {
6819        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6820        let workspace =
6821            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6822        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6823
6824        // Set hide_root to true
6825        cx.update(|_, cx| {
6826            let settings = *ProjectPanelSettings::get_global(cx);
6827            ProjectPanelSettings::override_global(
6828                ProjectPanelSettings {
6829                    hide_root: true,
6830                    ..settings
6831                },
6832                cx,
6833            );
6834        });
6835
6836        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6837        cx.run_until_parked();
6838
6839        assert_eq!(
6840            visible_entries_as_strings(&panel, 0..10, cx),
6841            &[
6842                "v root1",
6843                "    > dir1",
6844                "    > dir2",
6845                "      file4.txt",
6846                "v root2",
6847                "    > dir3",
6848                "      file6.txt",
6849            ],
6850            "With hide_root=true and multiple worktrees, roots should still be visible"
6851        );
6852    }
6853
6854    // Test 4: Multiple worktrees with hide_root = false
6855    {
6856        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6857        let workspace =
6858            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6859        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6860
6861        cx.update(|_, cx| {
6862            let settings = *ProjectPanelSettings::get_global(cx);
6863            ProjectPanelSettings::override_global(
6864                ProjectPanelSettings {
6865                    hide_root: false,
6866                    ..settings
6867                },
6868                cx,
6869            );
6870        });
6871
6872        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6873        cx.run_until_parked();
6874
6875        assert_eq!(
6876            visible_entries_as_strings(&panel, 0..10, cx),
6877            &[
6878                "v root1",
6879                "    > dir1",
6880                "    > dir2",
6881                "      file4.txt",
6882                "v root2",
6883                "    > dir3",
6884                "      file6.txt",
6885            ],
6886            "With hide_root=false and multiple worktrees, roots should be visible"
6887        );
6888    }
6889}
6890
6891#[gpui::test]
6892async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
6893    init_test_with_editor(cx);
6894
6895    let fs = FakeFs::new(cx.executor());
6896    fs.insert_tree(
6897        "/root",
6898        json!({
6899            "file1.txt": "content of file1",
6900            "file2.txt": "content of file2",
6901            "dir1": {
6902                "file3.txt": "content of file3"
6903            }
6904        }),
6905    )
6906    .await;
6907
6908    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6909    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6910    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6911    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6912    cx.run_until_parked();
6913
6914    let file1_path = "root/file1.txt";
6915    let file2_path = "root/file2.txt";
6916    select_path_with_mark(&panel, file1_path, cx);
6917    select_path_with_mark(&panel, file2_path, cx);
6918
6919    panel.update_in(cx, |panel, window, cx| {
6920        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
6921    });
6922    cx.executor().run_until_parked();
6923
6924    workspace
6925        .update(cx, |workspace, _, cx| {
6926            let active_items = workspace
6927                .panes()
6928                .iter()
6929                .filter_map(|pane| pane.read(cx).active_item())
6930                .collect::<Vec<_>>();
6931            assert_eq!(active_items.len(), 1);
6932            let diff_view = active_items
6933                .into_iter()
6934                .next()
6935                .unwrap()
6936                .downcast::<FileDiffView>()
6937                .expect("Open item should be an FileDiffView");
6938            assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
6939            assert_eq!(
6940                diff_view.tab_tooltip_text(cx).unwrap(),
6941                format!(
6942                    "{}{}",
6943                    rel_path(file1_path).display(PathStyle::local()),
6944                    rel_path(file2_path).display(PathStyle::local())
6945                )
6946            );
6947        })
6948        .unwrap();
6949
6950    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
6951    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
6952    let worktree_id = panel.update(cx, |panel, cx| {
6953        panel
6954            .project
6955            .read(cx)
6956            .worktrees(cx)
6957            .next()
6958            .unwrap()
6959            .read(cx)
6960            .id()
6961    });
6962
6963    let expected_entries = [
6964        SelectedEntry {
6965            worktree_id,
6966            entry_id: file1_entry_id,
6967        },
6968        SelectedEntry {
6969            worktree_id,
6970            entry_id: file2_entry_id,
6971        },
6972    ];
6973    panel.update(cx, |panel, _cx| {
6974        assert_eq!(
6975            &panel.marked_entries, &expected_entries,
6976            "Should keep marked entries after comparison"
6977        );
6978    });
6979
6980    panel.update(cx, |panel, cx| {
6981        panel.project.update(cx, |_, cx| {
6982            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
6983        })
6984    });
6985
6986    panel.update(cx, |panel, _cx| {
6987        assert_eq!(
6988            &panel.marked_entries, &expected_entries,
6989            "Marked entries should persist after focusing back on the project panel"
6990        );
6991    });
6992}
6993
6994#[gpui::test]
6995async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
6996    init_test_with_editor(cx);
6997
6998    let fs = FakeFs::new(cx.executor());
6999    fs.insert_tree(
7000        "/root",
7001        json!({
7002            "file1.txt": "content of file1",
7003            "file2.txt": "content of file2",
7004            "dir1": {},
7005            "dir2": {
7006                "file3.txt": "content of file3"
7007            }
7008        }),
7009    )
7010    .await;
7011
7012    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7013    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7014    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7015    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7016    cx.run_until_parked();
7017
7018    // Test 1: When only one file is selected, there should be no compare option
7019    select_path(&panel, "root/file1.txt", cx);
7020
7021    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7022    assert_eq!(
7023        selected_files, None,
7024        "Should not have compare option when only one file is selected"
7025    );
7026
7027    // Test 2: When multiple files are selected, there should be a compare option
7028    select_path_with_mark(&panel, "root/file1.txt", cx);
7029    select_path_with_mark(&panel, "root/file2.txt", cx);
7030
7031    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7032    assert!(
7033        selected_files.is_some(),
7034        "Should have files selected for comparison"
7035    );
7036    if let Some((file1, file2)) = selected_files {
7037        assert!(
7038            file1.to_string_lossy().ends_with("file1.txt")
7039                && file2.to_string_lossy().ends_with("file2.txt"),
7040            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
7041        );
7042    }
7043
7044    // Test 3: Selecting a directory shouldn't count as a comparable file
7045    select_path_with_mark(&panel, "root/dir1", cx);
7046
7047    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7048    assert!(
7049        selected_files.is_some(),
7050        "Directory selection should not affect comparable files"
7051    );
7052    if let Some((file1, file2)) = selected_files {
7053        assert!(
7054            file1.to_string_lossy().ends_with("file1.txt")
7055                && file2.to_string_lossy().ends_with("file2.txt"),
7056            "Selecting a directory should not affect the number of comparable files"
7057        );
7058    }
7059
7060    // Test 4: Selecting one more file
7061    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
7062
7063    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7064    assert!(
7065        selected_files.is_some(),
7066        "Directory selection should not affect comparable files"
7067    );
7068    if let Some((file1, file2)) = selected_files {
7069        assert!(
7070            file1.to_string_lossy().ends_with("file2.txt")
7071                && file2.to_string_lossy().ends_with("file3.txt"),
7072            "Selecting a directory should not affect the number of comparable files"
7073        );
7074    }
7075}
7076
7077#[gpui::test]
7078async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
7079    init_test(cx);
7080
7081    let fs = FakeFs::new(cx.executor());
7082    fs.insert_tree(
7083        "/root",
7084        json!({
7085            ".hidden-file.txt": "hidden file content",
7086            "visible-file.txt": "visible file content",
7087            ".hidden-parent-dir": {
7088                "nested-dir": {
7089                    "file.txt": "file content",
7090                }
7091            },
7092            "visible-dir": {
7093                "file-in-visible.txt": "file content",
7094                "nested": {
7095                    ".hidden-nested-dir": {
7096                        ".double-hidden-dir": {
7097                            "deep-file-1.txt": "deep content 1",
7098                            "deep-file-2.txt": "deep content 2"
7099                        },
7100                        "hidden-nested-file-1.txt": "hidden nested 1",
7101                        "hidden-nested-file-2.txt": "hidden nested 2"
7102                    },
7103                    "visible-nested-file.txt": "visible nested content"
7104                }
7105            }
7106        }),
7107    )
7108    .await;
7109
7110    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7111    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7112    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7113
7114    cx.update(|_, cx| {
7115        let settings = *ProjectPanelSettings::get_global(cx);
7116        ProjectPanelSettings::override_global(
7117            ProjectPanelSettings {
7118                hide_hidden: false,
7119                ..settings
7120            },
7121            cx,
7122        );
7123    });
7124
7125    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7126    cx.run_until_parked();
7127
7128    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
7129    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
7130    toggle_expand_dir(&panel, "root/visible-dir", cx);
7131    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
7132    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
7133    toggle_expand_dir(
7134        &panel,
7135        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
7136        cx,
7137    );
7138
7139    let expanded = [
7140        "v root",
7141        "    v .hidden-parent-dir",
7142        "        v nested-dir",
7143        "              file.txt",
7144        "    v visible-dir",
7145        "        v nested",
7146        "            v .hidden-nested-dir",
7147        "                v .double-hidden-dir  <== selected",
7148        "                      deep-file-1.txt",
7149        "                      deep-file-2.txt",
7150        "                  hidden-nested-file-1.txt",
7151        "                  hidden-nested-file-2.txt",
7152        "              visible-nested-file.txt",
7153        "          file-in-visible.txt",
7154        "      .hidden-file.txt",
7155        "      visible-file.txt",
7156    ];
7157
7158    assert_eq!(
7159        visible_entries_as_strings(&panel, 0..30, cx),
7160        &expanded,
7161        "With hide_hidden=false, contents of hidden nested directory should be visible"
7162    );
7163
7164    cx.update(|_, cx| {
7165        let settings = *ProjectPanelSettings::get_global(cx);
7166        ProjectPanelSettings::override_global(
7167            ProjectPanelSettings {
7168                hide_hidden: true,
7169                ..settings
7170            },
7171            cx,
7172        );
7173    });
7174
7175    panel.update_in(cx, |panel, window, cx| {
7176        panel.update_visible_entries(None, false, false, window, cx);
7177    });
7178    cx.run_until_parked();
7179
7180    assert_eq!(
7181        visible_entries_as_strings(&panel, 0..30, cx),
7182        &[
7183            "v root",
7184            "    v visible-dir",
7185            "        v nested",
7186            "              visible-nested-file.txt",
7187            "          file-in-visible.txt",
7188            "      visible-file.txt",
7189        ],
7190        "With hide_hidden=false, contents of hidden nested directory should be visible"
7191    );
7192
7193    panel.update_in(cx, |panel, window, cx| {
7194        let settings = *ProjectPanelSettings::get_global(cx);
7195        ProjectPanelSettings::override_global(
7196            ProjectPanelSettings {
7197                hide_hidden: false,
7198                ..settings
7199            },
7200            cx,
7201        );
7202        panel.update_visible_entries(None, false, false, window, cx);
7203    });
7204    cx.run_until_parked();
7205
7206    assert_eq!(
7207        visible_entries_as_strings(&panel, 0..30, cx),
7208        &expanded,
7209        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
7210    );
7211}
7212
7213fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7214    let path = rel_path(path);
7215    panel.update_in(cx, |panel, window, cx| {
7216        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7217            let worktree = worktree.read(cx);
7218            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7219                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7220                panel.update_visible_entries(
7221                    Some((worktree.id(), entry_id)),
7222                    false,
7223                    false,
7224                    window,
7225                    cx,
7226                );
7227                return;
7228            }
7229        }
7230        panic!("no worktree for path {:?}", path);
7231    });
7232    cx.run_until_parked();
7233}
7234
7235fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7236    let path = rel_path(path);
7237    panel.update(cx, |panel, cx| {
7238        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7239            let worktree = worktree.read(cx);
7240            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7241                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7242                let entry = crate::SelectedEntry {
7243                    worktree_id: worktree.id(),
7244                    entry_id,
7245                };
7246                if !panel.marked_entries.contains(&entry) {
7247                    panel.marked_entries.push(entry);
7248                }
7249                panel.state.selection = Some(entry);
7250                return;
7251            }
7252        }
7253        panic!("no worktree for path {:?}", path);
7254    });
7255}
7256
7257fn find_project_entry(
7258    panel: &Entity<ProjectPanel>,
7259    path: &str,
7260    cx: &mut VisualTestContext,
7261) -> Option<ProjectEntryId> {
7262    let path = rel_path(path);
7263    panel.update(cx, |panel, cx| {
7264        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7265            let worktree = worktree.read(cx);
7266            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7267                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
7268            }
7269        }
7270        panic!("no worktree for path {path:?}");
7271    })
7272}
7273
7274fn visible_entries_as_strings(
7275    panel: &Entity<ProjectPanel>,
7276    range: Range<usize>,
7277    cx: &mut VisualTestContext,
7278) -> Vec<String> {
7279    let mut result = Vec::new();
7280    let mut project_entries = HashSet::default();
7281    let mut has_editor = false;
7282
7283    panel.update_in(cx, |panel, window, cx| {
7284        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
7285            if details.is_editing {
7286                assert!(!has_editor, "duplicate editor entry");
7287                has_editor = true;
7288            } else {
7289                assert!(
7290                    project_entries.insert(project_entry),
7291                    "duplicate project entry {:?} {:?}",
7292                    project_entry,
7293                    details
7294                );
7295            }
7296
7297            let indent = "    ".repeat(details.depth);
7298            let icon = if details.kind.is_dir() {
7299                if details.is_expanded { "v " } else { "> " }
7300            } else {
7301                "  "
7302            };
7303            #[cfg(windows)]
7304            let filename = details.filename.replace("\\", "/");
7305            #[cfg(not(windows))]
7306            let filename = details.filename;
7307            let name = if details.is_editing {
7308                format!("[EDITOR: '{}']", filename)
7309            } else if details.is_processing {
7310                format!("[PROCESSING: '{}']", filename)
7311            } else {
7312                filename
7313            };
7314            let selected = if details.is_selected {
7315                "  <== selected"
7316            } else {
7317                ""
7318            };
7319            let marked = if details.is_marked {
7320                "  <== marked"
7321            } else {
7322                ""
7323            };
7324
7325            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
7326        });
7327    });
7328
7329    result
7330}
7331
7332fn init_test(cx: &mut TestAppContext) {
7333    cx.update(|cx| {
7334        let settings_store = SettingsStore::test(cx);
7335        cx.set_global(settings_store);
7336        theme::init(theme::LoadThemes::JustBase, cx);
7337        crate::init(cx);
7338
7339        cx.update_global::<SettingsStore, _>(|store, cx| {
7340            store.update_user_settings(cx, |settings| {
7341                settings
7342                    .project_panel
7343                    .get_or_insert_default()
7344                    .auto_fold_dirs = Some(false);
7345                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
7346            });
7347        });
7348    });
7349}
7350
7351fn init_test_with_editor(cx: &mut TestAppContext) {
7352    cx.update(|cx| {
7353        let app_state = AppState::test(cx);
7354        theme::init(theme::LoadThemes::JustBase, cx);
7355        editor::init(cx);
7356        crate::init(cx);
7357        workspace::init(app_state, cx);
7358
7359        cx.update_global::<SettingsStore, _>(|store, cx| {
7360            store.update_user_settings(cx, |settings| {
7361                settings
7362                    .project_panel
7363                    .get_or_insert_default()
7364                    .auto_fold_dirs = Some(false);
7365                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
7366            });
7367        });
7368    });
7369}
7370
7371fn ensure_single_file_is_opened(
7372    window: &WindowHandle<Workspace>,
7373    expected_path: &str,
7374    cx: &mut TestAppContext,
7375) {
7376    window
7377        .update(cx, |workspace, _, cx| {
7378            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
7379            assert_eq!(worktrees.len(), 1);
7380            let worktree_id = worktrees[0].read(cx).id();
7381
7382            let open_project_paths = workspace
7383                .panes()
7384                .iter()
7385                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
7386                .collect::<Vec<_>>();
7387            assert_eq!(
7388                open_project_paths,
7389                vec![ProjectPath {
7390                    worktree_id,
7391                    path: Arc::from(rel_path(expected_path))
7392                }],
7393                "Should have opened file, selected in project panel"
7394            );
7395        })
7396        .unwrap();
7397}
7398
7399fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
7400    assert!(
7401        !cx.has_pending_prompt(),
7402        "Should have no prompts before the deletion"
7403    );
7404    panel.update_in(cx, |panel, window, cx| {
7405        panel.delete(&Delete { skip_prompt: false }, window, cx)
7406    });
7407    assert!(
7408        cx.has_pending_prompt(),
7409        "Should have a prompt after the deletion"
7410    );
7411    cx.simulate_prompt_answer("Delete");
7412    assert!(
7413        !cx.has_pending_prompt(),
7414        "Should have no prompts after prompt was replied to"
7415    );
7416    cx.executor().run_until_parked();
7417}
7418
7419fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
7420    assert!(
7421        !cx.has_pending_prompt(),
7422        "Should have no prompts before the deletion"
7423    );
7424    panel.update_in(cx, |panel, window, cx| {
7425        panel.delete(&Delete { skip_prompt: true }, window, cx)
7426    });
7427    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
7428    cx.executor().run_until_parked();
7429}
7430
7431fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
7432    assert!(
7433        !cx.has_pending_prompt(),
7434        "Should have no prompts after deletion operation closes the file"
7435    );
7436    workspace
7437        .read_with(cx, |workspace, cx| {
7438            let open_project_paths = workspace
7439                .panes()
7440                .iter()
7441                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
7442                .collect::<Vec<_>>();
7443            assert!(
7444                open_project_paths.is_empty(),
7445                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
7446            );
7447        })
7448        .unwrap();
7449}
7450
7451struct TestProjectItemView {
7452    focus_handle: FocusHandle,
7453    path: ProjectPath,
7454}
7455
7456struct TestProjectItem {
7457    path: ProjectPath,
7458}
7459
7460impl project::ProjectItem for TestProjectItem {
7461    fn try_open(
7462        _project: &Entity<Project>,
7463        path: &ProjectPath,
7464        cx: &mut App,
7465    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
7466        let path = path.clone();
7467        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
7468    }
7469
7470    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
7471        None
7472    }
7473
7474    fn project_path(&self, _: &App) -> Option<ProjectPath> {
7475        Some(self.path.clone())
7476    }
7477
7478    fn is_dirty(&self) -> bool {
7479        false
7480    }
7481}
7482
7483impl ProjectItem for TestProjectItemView {
7484    type Item = TestProjectItem;
7485
7486    fn for_project_item(
7487        _: Entity<Project>,
7488        _: Option<&Pane>,
7489        project_item: Entity<Self::Item>,
7490        _: &mut Window,
7491        cx: &mut Context<Self>,
7492    ) -> Self
7493    where
7494        Self: Sized,
7495    {
7496        Self {
7497            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
7498            focus_handle: cx.focus_handle(),
7499        }
7500    }
7501}
7502
7503impl Item for TestProjectItemView {
7504    type Event = ();
7505
7506    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
7507        "Test".into()
7508    }
7509}
7510
7511impl EventEmitter<()> for TestProjectItemView {}
7512
7513impl Focusable for TestProjectItemView {
7514    fn focus_handle(&self, _: &App) -> FocusHandle {
7515        self.focus_handle.clone()
7516    }
7517}
7518
7519impl Render for TestProjectItemView {
7520    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7521        Empty
7522    }
7523}