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::{ProjectPanelAutoOpenSettings, 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, None, 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, None, 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_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) {
2003    init_test_with_editor(cx);
2004    set_auto_open_settings(
2005        cx,
2006        ProjectPanelAutoOpenSettings {
2007            on_create: Some(true),
2008            ..Default::default()
2009        },
2010    );
2011
2012    let fs = FakeFs::new(cx.executor());
2013    fs.insert_tree(path!("/root"), json!({})).await;
2014
2015    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2016    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2017    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2018    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2019    cx.run_until_parked();
2020
2021    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2022    cx.run_until_parked();
2023    panel
2024        .update_in(cx, |panel, window, cx| {
2025            panel.filename_editor.update(cx, |editor, cx| {
2026                editor.set_text("auto-open.rs", window, cx);
2027            });
2028            panel.confirm_edit(true, window, cx).unwrap()
2029        })
2030        .await
2031        .unwrap();
2032    cx.run_until_parked();
2033
2034    ensure_single_file_is_opened(&workspace, "auto-open.rs", cx);
2035}
2036
2037#[gpui::test]
2038async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) {
2039    init_test_with_editor(cx);
2040    set_auto_open_settings(
2041        cx,
2042        ProjectPanelAutoOpenSettings {
2043            on_create: Some(false),
2044            ..Default::default()
2045        },
2046    );
2047
2048    let fs = FakeFs::new(cx.executor());
2049    fs.insert_tree(path!("/root"), json!({})).await;
2050
2051    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2052    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2053    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2054    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2055    cx.run_until_parked();
2056
2057    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2058    cx.run_until_parked();
2059    panel
2060        .update_in(cx, |panel, window, cx| {
2061            panel.filename_editor.update(cx, |editor, cx| {
2062                editor.set_text("manual-open.rs", window, cx);
2063            });
2064            panel.confirm_edit(true, window, cx).unwrap()
2065        })
2066        .await
2067        .unwrap();
2068    cx.run_until_parked();
2069
2070    ensure_no_open_items_and_panes(&workspace, cx);
2071}
2072
2073#[gpui::test]
2074async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) {
2075    init_test_with_editor(cx);
2076    set_auto_open_settings(
2077        cx,
2078        ProjectPanelAutoOpenSettings {
2079            on_paste: Some(true),
2080            ..Default::default()
2081        },
2082    );
2083
2084    let fs = FakeFs::new(cx.executor());
2085    fs.insert_tree(
2086        path!("/root"),
2087        json!({
2088            "src": {
2089                "original.rs": ""
2090            },
2091            "target": {}
2092        }),
2093    )
2094    .await;
2095
2096    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2097    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2098    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2099    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2100    cx.run_until_parked();
2101
2102    toggle_expand_dir(&panel, "root/src", cx);
2103    toggle_expand_dir(&panel, "root/target", cx);
2104
2105    select_path(&panel, "root/src/original.rs", cx);
2106    panel.update_in(cx, |panel, window, cx| {
2107        panel.copy(&Default::default(), window, cx);
2108    });
2109
2110    select_path(&panel, "root/target", cx);
2111    panel.update_in(cx, |panel, window, cx| {
2112        panel.paste(&Default::default(), window, cx);
2113    });
2114    cx.executor().run_until_parked();
2115
2116    ensure_single_file_is_opened(&workspace, "target/original.rs", cx);
2117}
2118
2119#[gpui::test]
2120async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) {
2121    init_test_with_editor(cx);
2122    set_auto_open_settings(
2123        cx,
2124        ProjectPanelAutoOpenSettings {
2125            on_paste: Some(false),
2126            ..Default::default()
2127        },
2128    );
2129
2130    let fs = FakeFs::new(cx.executor());
2131    fs.insert_tree(
2132        path!("/root"),
2133        json!({
2134            "src": {
2135                "original.rs": ""
2136            },
2137            "target": {}
2138        }),
2139    )
2140    .await;
2141
2142    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2143    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2144    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2145    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2146    cx.run_until_parked();
2147
2148    toggle_expand_dir(&panel, "root/src", cx);
2149    toggle_expand_dir(&panel, "root/target", cx);
2150
2151    select_path(&panel, "root/src/original.rs", cx);
2152    panel.update_in(cx, |panel, window, cx| {
2153        panel.copy(&Default::default(), window, cx);
2154    });
2155
2156    select_path(&panel, "root/target", cx);
2157    panel.update_in(cx, |panel, window, cx| {
2158        panel.paste(&Default::default(), window, cx);
2159    });
2160    cx.executor().run_until_parked();
2161
2162    ensure_no_open_items_and_panes(&workspace, cx);
2163    assert!(
2164        find_project_entry(&panel, "root/target/original.rs", cx).is_some(),
2165        "Pasted entry should exist even when auto-open is disabled"
2166    );
2167}
2168
2169#[gpui::test]
2170async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) {
2171    init_test_with_editor(cx);
2172    set_auto_open_settings(
2173        cx,
2174        ProjectPanelAutoOpenSettings {
2175            on_drop: Some(true),
2176            ..Default::default()
2177        },
2178    );
2179
2180    let fs = FakeFs::new(cx.executor());
2181    fs.insert_tree(path!("/root"), json!({})).await;
2182
2183    let temp_dir = tempfile::tempdir().unwrap();
2184    let external_path = temp_dir.path().join("dropped.rs");
2185    std::fs::write(&external_path, "// dropped").unwrap();
2186    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2187        .await;
2188
2189    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2190    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2191    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2192    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2193    cx.run_until_parked();
2194
2195    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2196    panel.update_in(cx, |panel, window, cx| {
2197        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2198    });
2199    cx.executor().run_until_parked();
2200
2201    ensure_single_file_is_opened(&workspace, "dropped.rs", cx);
2202}
2203
2204#[gpui::test]
2205async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) {
2206    init_test_with_editor(cx);
2207    set_auto_open_settings(
2208        cx,
2209        ProjectPanelAutoOpenSettings {
2210            on_drop: Some(false),
2211            ..Default::default()
2212        },
2213    );
2214
2215    let fs = FakeFs::new(cx.executor());
2216    fs.insert_tree(path!("/root"), json!({})).await;
2217
2218    let temp_dir = tempfile::tempdir().unwrap();
2219    let external_path = temp_dir.path().join("manual.rs");
2220    std::fs::write(&external_path, "// dropped").unwrap();
2221    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2222        .await;
2223
2224    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2225    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2226    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2227    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2228    cx.run_until_parked();
2229
2230    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2231    panel.update_in(cx, |panel, window, cx| {
2232        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2233    });
2234    cx.executor().run_until_parked();
2235
2236    ensure_no_open_items_and_panes(&workspace, cx);
2237    assert!(
2238        find_project_entry(&panel, "root/manual.rs", cx).is_some(),
2239        "Dropped entry should exist even when auto-open is disabled"
2240    );
2241}
2242
2243#[gpui::test]
2244async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2245    init_test_with_editor(cx);
2246
2247    let fs = FakeFs::new(cx.executor());
2248    fs.insert_tree(
2249        "/src",
2250        json!({
2251            "test": {
2252                "first.rs": "// First Rust file",
2253                "second.rs": "// Second Rust file",
2254                "third.rs": "// Third Rust file",
2255            }
2256        }),
2257    )
2258    .await;
2259
2260    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2261    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2262    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2263    let panel = workspace
2264        .update(cx, |workspace, window, cx| {
2265            let panel = ProjectPanel::new(workspace, window, cx);
2266            workspace.add_panel(panel.clone(), window, cx);
2267            panel
2268        })
2269        .unwrap();
2270    cx.run_until_parked();
2271
2272    select_path(&panel, "src", cx);
2273    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2274    cx.executor().run_until_parked();
2275    assert_eq!(
2276        visible_entries_as_strings(&panel, 0..10, cx),
2277        &[
2278            //
2279            "v src  <== selected",
2280            "    > test"
2281        ]
2282    );
2283    panel.update_in(cx, |panel, window, cx| {
2284        panel.new_directory(&NewDirectory, window, cx)
2285    });
2286    cx.run_until_parked();
2287    panel.update_in(cx, |panel, window, cx| {
2288        assert!(panel.filename_editor.read(cx).is_focused(window));
2289    });
2290    cx.executor().run_until_parked();
2291    assert_eq!(
2292        visible_entries_as_strings(&panel, 0..10, cx),
2293        &[
2294            //
2295            "v src",
2296            "    > [EDITOR: '']  <== selected",
2297            "    > test"
2298        ]
2299    );
2300    panel.update_in(cx, |panel, window, cx| {
2301        panel
2302            .filename_editor
2303            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2304        assert!(
2305            panel.confirm_edit(true, window, cx).is_none(),
2306            "Should not allow to confirm on conflicting new directory name"
2307        );
2308    });
2309    cx.executor().run_until_parked();
2310    panel.update_in(cx, |panel, window, cx| {
2311        assert!(
2312            panel.state.edit_state.is_some(),
2313            "Edit state should not be None after conflicting new directory name"
2314        );
2315        panel.cancel(&menu::Cancel, window, cx);
2316        panel.update_visible_entries(None, None, false, window, cx);
2317    });
2318    cx.run_until_parked();
2319    assert_eq!(
2320        visible_entries_as_strings(&panel, 0..10, cx),
2321        &[
2322            //
2323            "v src  <== selected",
2324            "    > test"
2325        ],
2326        "File list should be unchanged after failed folder create confirmation"
2327    );
2328
2329    select_path(&panel, "src/test", cx);
2330    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2331    cx.executor().run_until_parked();
2332    assert_eq!(
2333        visible_entries_as_strings(&panel, 0..10, cx),
2334        &[
2335            //
2336            "v src",
2337            "    > test  <== selected"
2338        ]
2339    );
2340    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2341    cx.run_until_parked();
2342    panel.update_in(cx, |panel, window, cx| {
2343        assert!(panel.filename_editor.read(cx).is_focused(window));
2344    });
2345    assert_eq!(
2346        visible_entries_as_strings(&panel, 0..10, cx),
2347        &[
2348            "v src",
2349            "    v test",
2350            "          [EDITOR: '']  <== selected",
2351            "          first.rs",
2352            "          second.rs",
2353            "          third.rs"
2354        ]
2355    );
2356    panel.update_in(cx, |panel, window, cx| {
2357        panel
2358            .filename_editor
2359            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2360        assert!(
2361            panel.confirm_edit(true, window, cx).is_none(),
2362            "Should not allow to confirm on conflicting new file name"
2363        );
2364    });
2365    cx.executor().run_until_parked();
2366    panel.update_in(cx, |panel, window, cx| {
2367        assert!(
2368            panel.state.edit_state.is_some(),
2369            "Edit state should not be None after conflicting new file name"
2370        );
2371        panel.cancel(&menu::Cancel, window, cx);
2372        panel.update_visible_entries(None, None, false, window, cx);
2373    });
2374    cx.run_until_parked();
2375    assert_eq!(
2376        visible_entries_as_strings(&panel, 0..10, cx),
2377        &[
2378            "v src",
2379            "    v test  <== selected",
2380            "          first.rs",
2381            "          second.rs",
2382            "          third.rs"
2383        ],
2384        "File list should be unchanged after failed file create confirmation"
2385    );
2386
2387    select_path(&panel, "src/test/first.rs", cx);
2388    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2389    cx.executor().run_until_parked();
2390    assert_eq!(
2391        visible_entries_as_strings(&panel, 0..10, cx),
2392        &[
2393            "v src",
2394            "    v test",
2395            "          first.rs  <== selected",
2396            "          second.rs",
2397            "          third.rs"
2398        ],
2399    );
2400    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2401    panel.update_in(cx, |panel, window, cx| {
2402        assert!(panel.filename_editor.read(cx).is_focused(window));
2403    });
2404    assert_eq!(
2405        visible_entries_as_strings(&panel, 0..10, cx),
2406        &[
2407            "v src",
2408            "    v test",
2409            "          [EDITOR: 'first.rs']  <== selected",
2410            "          second.rs",
2411            "          third.rs"
2412        ]
2413    );
2414    panel.update_in(cx, |panel, window, cx| {
2415        panel
2416            .filename_editor
2417            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2418        assert!(
2419            panel.confirm_edit(true, window, cx).is_none(),
2420            "Should not allow to confirm on conflicting file rename"
2421        )
2422    });
2423    cx.executor().run_until_parked();
2424    panel.update_in(cx, |panel, window, cx| {
2425        assert!(
2426            panel.state.edit_state.is_some(),
2427            "Edit state should not be None after conflicting file rename"
2428        );
2429        panel.cancel(&menu::Cancel, window, cx);
2430    });
2431    assert_eq!(
2432        visible_entries_as_strings(&panel, 0..10, cx),
2433        &[
2434            "v src",
2435            "    v test",
2436            "          first.rs  <== selected",
2437            "          second.rs",
2438            "          third.rs"
2439        ],
2440        "File list should be unchanged after failed rename confirmation"
2441    );
2442}
2443
2444// NOTE: This test is skipped on Windows, because on Windows,
2445// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
2446// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
2447#[gpui::test]
2448#[cfg_attr(target_os = "windows", ignore)]
2449async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
2450    init_test_with_editor(cx);
2451
2452    let fs = FakeFs::new(cx.executor());
2453    fs.insert_tree(
2454        "/src",
2455        json!({
2456            "test": {
2457                "first.txt": "// First Txt file",
2458                "second.txt": "// Second Txt file",
2459                "third.txt": "// Third Txt file",
2460            }
2461        }),
2462    )
2463    .await;
2464
2465    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2466    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2467    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2468    let panel = workspace
2469        .update(cx, |workspace, window, cx| {
2470            let panel = ProjectPanel::new(workspace, window, cx);
2471            workspace.add_panel(panel.clone(), window, cx);
2472            panel
2473        })
2474        .unwrap();
2475    cx.run_until_parked();
2476
2477    select_path(&panel, "src", cx);
2478    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2479    cx.executor().run_until_parked();
2480    assert_eq!(
2481        visible_entries_as_strings(&panel, 0..10, cx),
2482        &[
2483            //
2484            "v src  <== selected",
2485            "    > test"
2486        ]
2487    );
2488    panel.update_in(cx, |panel, window, cx| {
2489        panel.new_directory(&NewDirectory, window, cx)
2490    });
2491    cx.run_until_parked();
2492    panel.update_in(cx, |panel, window, cx| {
2493        assert!(panel.filename_editor.read(cx).is_focused(window));
2494    });
2495    cx.executor().run_until_parked();
2496    assert_eq!(
2497        visible_entries_as_strings(&panel, 0..10, cx),
2498        &[
2499            //
2500            "v src",
2501            "    > [EDITOR: '']  <== selected",
2502            "    > test"
2503        ]
2504    );
2505    panel.update_in(cx, |panel, window, cx| {
2506        panel
2507            .filename_editor
2508            .update(cx, |editor, cx| editor.set_text("test", window, cx));
2509        assert!(
2510            panel.confirm_edit(true, window, cx).is_none(),
2511            "Should not allow to confirm on conflicting new directory name"
2512        );
2513    });
2514    cx.executor().run_until_parked();
2515    panel.update_in(cx, |panel, window, cx| {
2516        assert!(
2517            panel.state.edit_state.is_some(),
2518            "Edit state should not be None after conflicting new directory name"
2519        );
2520        panel.cancel(&menu::Cancel, window, cx);
2521        panel.update_visible_entries(None, None, false, window, cx);
2522    });
2523    cx.run_until_parked();
2524    assert_eq!(
2525        visible_entries_as_strings(&panel, 0..10, cx),
2526        &[
2527            //
2528            "v src  <== selected",
2529            "    > test"
2530        ],
2531        "File list should be unchanged after failed folder create confirmation"
2532    );
2533
2534    select_path(&panel, "src/test", cx);
2535    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2536    cx.executor().run_until_parked();
2537    assert_eq!(
2538        visible_entries_as_strings(&panel, 0..10, cx),
2539        &[
2540            //
2541            "v src",
2542            "    > test  <== selected"
2543        ]
2544    );
2545    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2546    cx.run_until_parked();
2547    panel.update_in(cx, |panel, window, cx| {
2548        assert!(panel.filename_editor.read(cx).is_focused(window));
2549    });
2550    assert_eq!(
2551        visible_entries_as_strings(&panel, 0..10, cx),
2552        &[
2553            "v src",
2554            "    v test",
2555            "          [EDITOR: '']  <== selected",
2556            "          first.txt",
2557            "          second.txt",
2558            "          third.txt"
2559        ]
2560    );
2561    panel.update_in(cx, |panel, window, cx| {
2562        panel
2563            .filename_editor
2564            .update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
2565        assert!(
2566            panel.confirm_edit(true, window, cx).is_none(),
2567            "Should not allow to confirm on conflicting new file name"
2568        );
2569    });
2570    cx.executor().run_until_parked();
2571    panel.update_in(cx, |panel, window, cx| {
2572        assert!(
2573            panel.state.edit_state.is_some(),
2574            "Edit state should not be None after conflicting new file name"
2575        );
2576        panel.cancel(&menu::Cancel, window, cx);
2577        panel.update_visible_entries(None, None, false, window, cx);
2578    });
2579    cx.run_until_parked();
2580    assert_eq!(
2581        visible_entries_as_strings(&panel, 0..10, cx),
2582        &[
2583            "v src",
2584            "    v test  <== selected",
2585            "          first.txt",
2586            "          second.txt",
2587            "          third.txt"
2588        ],
2589        "File list should be unchanged after failed file create confirmation"
2590    );
2591
2592    select_path(&panel, "src/test/first.txt", cx);
2593    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2594    cx.executor().run_until_parked();
2595    assert_eq!(
2596        visible_entries_as_strings(&panel, 0..10, cx),
2597        &[
2598            "v src",
2599            "    v test",
2600            "          first.txt  <== selected",
2601            "          second.txt",
2602            "          third.txt"
2603        ],
2604    );
2605    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2606    panel.update_in(cx, |panel, window, cx| {
2607        assert!(panel.filename_editor.read(cx).is_focused(window));
2608    });
2609    assert_eq!(
2610        visible_entries_as_strings(&panel, 0..10, cx),
2611        &[
2612            "v src",
2613            "    v test",
2614            "          [EDITOR: 'first.txt']  <== selected",
2615            "          second.txt",
2616            "          third.txt"
2617        ]
2618    );
2619    panel.update_in(cx, |panel, window, cx| {
2620        panel
2621            .filename_editor
2622            .update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
2623        assert!(
2624            panel.confirm_edit(true, window, cx).is_none(),
2625            "Should not allow to confirm on conflicting file rename"
2626        )
2627    });
2628    cx.executor().run_until_parked();
2629    panel.update_in(cx, |panel, window, cx| {
2630        assert!(
2631            panel.state.edit_state.is_some(),
2632            "Edit state should not be None after conflicting file rename"
2633        );
2634        panel.cancel(&menu::Cancel, window, cx);
2635    });
2636    assert_eq!(
2637        visible_entries_as_strings(&panel, 0..10, cx),
2638        &[
2639            "v src",
2640            "    v test",
2641            "          first.txt  <== selected",
2642            "          second.txt",
2643            "          third.txt"
2644        ],
2645        "File list should be unchanged after failed rename confirmation"
2646    );
2647    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2648    cx.executor().run_until_parked();
2649    // Try to duplicate and check history
2650    panel.update_in(cx, |panel, window, cx| {
2651        panel.duplicate(&Duplicate, window, cx)
2652    });
2653    cx.executor().run_until_parked();
2654
2655    assert_eq!(
2656        visible_entries_as_strings(&panel, 0..10, cx),
2657        &[
2658            "v src",
2659            "    v test",
2660            "          first.txt",
2661            "          [EDITOR: 'first copy.txt']  <== selected  <== marked",
2662            "          second.txt",
2663            "          third.txt"
2664        ],
2665    );
2666
2667    let confirm = panel.update_in(cx, |panel, window, cx| {
2668        panel
2669            .filename_editor
2670            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2671        panel.confirm_edit(true, window, cx).unwrap()
2672    });
2673    confirm.await.unwrap();
2674    cx.executor().run_until_parked();
2675
2676    assert_eq!(
2677        visible_entries_as_strings(&panel, 0..10, cx),
2678        &[
2679            "v src",
2680            "    v test",
2681            "          first.txt",
2682            "          fourth.txt  <== selected",
2683            "          second.txt",
2684            "          third.txt"
2685        ],
2686        "File list should be different after rename confirmation"
2687    );
2688
2689    panel.update_in(cx, |panel, window, cx| {
2690        panel.update_visible_entries(None, None, false, window, cx);
2691    });
2692    cx.executor().run_until_parked();
2693
2694    select_path(&panel, "src/test/first.txt", cx);
2695    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2696    cx.executor().run_until_parked();
2697
2698    workspace
2699        .read_with(cx, |this, cx| {
2700            assert!(
2701                this.recent_navigation_history_iter(cx)
2702                    .any(|(project_path, abs_path)| {
2703                        project_path.path == Arc::from(rel_path("test/fourth.txt"))
2704                            && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2705                    })
2706            );
2707        })
2708        .unwrap();
2709}
2710
2711// NOTE: This test is skipped on Windows, because on Windows,
2712// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
2713// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
2714#[gpui::test]
2715#[cfg_attr(target_os = "windows", ignore)]
2716async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
2717    init_test_with_editor(cx);
2718
2719    let fs = FakeFs::new(cx.executor());
2720    fs.insert_tree(
2721        "/src",
2722        json!({
2723            "test": {
2724                "first.txt": "// First Txt file",
2725                "second.txt": "// Second Txt file",
2726                "third.txt": "// Third Txt file",
2727            }
2728        }),
2729    )
2730    .await;
2731
2732    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2733    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2734    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2735    let panel = workspace
2736        .update(cx, |workspace, window, cx| {
2737            let panel = ProjectPanel::new(workspace, window, cx);
2738            workspace.add_panel(panel.clone(), window, cx);
2739            panel
2740        })
2741        .unwrap();
2742    cx.run_until_parked();
2743
2744    select_path(&panel, "src", cx);
2745    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2746    cx.executor().run_until_parked();
2747    assert_eq!(
2748        visible_entries_as_strings(&panel, 0..10, cx),
2749        &[
2750            //
2751            "v src  <== selected",
2752            "    > test"
2753        ]
2754    );
2755
2756    select_path(&panel, "src/test", cx);
2757    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2758    cx.executor().run_until_parked();
2759    assert_eq!(
2760        visible_entries_as_strings(&panel, 0..10, cx),
2761        &[
2762            //
2763            "v src",
2764            "    > test  <== selected"
2765        ]
2766    );
2767    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2768    cx.run_until_parked();
2769    panel.update_in(cx, |panel, window, cx| {
2770        assert!(panel.filename_editor.read(cx).is_focused(window));
2771    });
2772
2773    select_path(&panel, "src/test/first.txt", cx);
2774    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2775    cx.executor().run_until_parked();
2776
2777    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2778    cx.executor().run_until_parked();
2779
2780    assert_eq!(
2781        visible_entries_as_strings(&panel, 0..10, cx),
2782        &[
2783            "v src",
2784            "    v test",
2785            "          [EDITOR: 'first.txt']  <== selected  <== marked",
2786            "          second.txt",
2787            "          third.txt"
2788        ],
2789    );
2790
2791    let confirm = panel.update_in(cx, |panel, window, cx| {
2792        panel
2793            .filename_editor
2794            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2795        panel.confirm_edit(true, window, cx).unwrap()
2796    });
2797    confirm.await.unwrap();
2798    cx.executor().run_until_parked();
2799
2800    assert_eq!(
2801        visible_entries_as_strings(&panel, 0..10, cx),
2802        &[
2803            "v src",
2804            "    v test",
2805            "          fourth.txt  <== selected",
2806            "          second.txt",
2807            "          third.txt"
2808        ],
2809        "File list should be different after rename confirmation"
2810    );
2811
2812    panel.update_in(cx, |panel, window, cx| {
2813        panel.update_visible_entries(None, None, false, window, cx);
2814    });
2815    cx.executor().run_until_parked();
2816
2817    select_path(&panel, "src/test/second.txt", cx);
2818    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2819    cx.executor().run_until_parked();
2820
2821    workspace
2822        .read_with(cx, |this, cx| {
2823            assert!(
2824                this.recent_navigation_history_iter(cx)
2825                    .any(|(project_path, abs_path)| {
2826                        project_path.path == Arc::from(rel_path("test/fourth.txt"))
2827                            && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2828                    })
2829            );
2830        })
2831        .unwrap();
2832}
2833
2834#[gpui::test]
2835async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2836    init_test_with_editor(cx);
2837
2838    let fs = FakeFs::new(cx.executor());
2839    fs.insert_tree(
2840        path!("/root"),
2841        json!({
2842            "tree1": {
2843                ".git": {},
2844                "dir1": {
2845                    "modified1.txt": "1",
2846                    "unmodified1.txt": "1",
2847                    "modified2.txt": "1",
2848                },
2849                "dir2": {
2850                    "modified3.txt": "1",
2851                    "unmodified2.txt": "1",
2852                },
2853                "modified4.txt": "1",
2854                "unmodified3.txt": "1",
2855            },
2856            "tree2": {
2857                ".git": {},
2858                "dir3": {
2859                    "modified5.txt": "1",
2860                    "unmodified4.txt": "1",
2861                },
2862                "modified6.txt": "1",
2863                "unmodified5.txt": "1",
2864            }
2865        }),
2866    )
2867    .await;
2868
2869    // Mark files as git modified
2870    fs.set_head_and_index_for_repo(
2871        path!("/root/tree1/.git").as_ref(),
2872        &[
2873            ("dir1/modified1.txt", "modified".into()),
2874            ("dir1/modified2.txt", "modified".into()),
2875            ("modified4.txt", "modified".into()),
2876            ("dir2/modified3.txt", "modified".into()),
2877        ],
2878    );
2879    fs.set_head_and_index_for_repo(
2880        path!("/root/tree2/.git").as_ref(),
2881        &[
2882            ("dir3/modified5.txt", "modified".into()),
2883            ("modified6.txt", "modified".into()),
2884        ],
2885    );
2886
2887    let project = Project::test(
2888        fs.clone(),
2889        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2890        cx,
2891    )
2892    .await;
2893
2894    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2895        let mut worktrees = project.worktrees(cx);
2896        let worktree1 = worktrees.next().unwrap();
2897        let worktree2 = worktrees.next().unwrap();
2898        (
2899            worktree1.read(cx).as_local().unwrap().scan_complete(),
2900            worktree2.read(cx).as_local().unwrap().scan_complete(),
2901        )
2902    });
2903    scan1_complete.await;
2904    scan2_complete.await;
2905    cx.run_until_parked();
2906
2907    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2908    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2909    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2910    cx.run_until_parked();
2911
2912    // Check initial state
2913    assert_eq!(
2914        visible_entries_as_strings(&panel, 0..15, cx),
2915        &[
2916            "v tree1",
2917            "    > .git",
2918            "    > dir1",
2919            "    > dir2",
2920            "      modified4.txt",
2921            "      unmodified3.txt",
2922            "v tree2",
2923            "    > .git",
2924            "    > dir3",
2925            "      modified6.txt",
2926            "      unmodified5.txt"
2927        ],
2928    );
2929
2930    // Test selecting next modified entry
2931    panel.update_in(cx, |panel, window, cx| {
2932        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2933    });
2934    cx.run_until_parked();
2935
2936    assert_eq!(
2937        visible_entries_as_strings(&panel, 0..6, cx),
2938        &[
2939            "v tree1",
2940            "    > .git",
2941            "    v dir1",
2942            "          modified1.txt  <== selected",
2943            "          modified2.txt",
2944            "          unmodified1.txt",
2945        ],
2946    );
2947
2948    panel.update_in(cx, |panel, window, cx| {
2949        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2950    });
2951    cx.run_until_parked();
2952
2953    assert_eq!(
2954        visible_entries_as_strings(&panel, 0..6, cx),
2955        &[
2956            "v tree1",
2957            "    > .git",
2958            "    v dir1",
2959            "          modified1.txt",
2960            "          modified2.txt  <== selected",
2961            "          unmodified1.txt",
2962        ],
2963    );
2964
2965    panel.update_in(cx, |panel, window, cx| {
2966        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2967    });
2968    cx.run_until_parked();
2969
2970    assert_eq!(
2971        visible_entries_as_strings(&panel, 6..9, cx),
2972        &[
2973            "    v dir2",
2974            "          modified3.txt  <== selected",
2975            "          unmodified2.txt",
2976        ],
2977    );
2978
2979    panel.update_in(cx, |panel, window, cx| {
2980        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2981    });
2982    cx.run_until_parked();
2983
2984    assert_eq!(
2985        visible_entries_as_strings(&panel, 9..11, cx),
2986        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2987    );
2988
2989    panel.update_in(cx, |panel, window, cx| {
2990        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2991    });
2992    cx.run_until_parked();
2993
2994    assert_eq!(
2995        visible_entries_as_strings(&panel, 13..16, cx),
2996        &[
2997            "    v dir3",
2998            "          modified5.txt  <== selected",
2999            "          unmodified4.txt",
3000        ],
3001    );
3002
3003    panel.update_in(cx, |panel, window, cx| {
3004        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3005    });
3006    cx.run_until_parked();
3007
3008    assert_eq!(
3009        visible_entries_as_strings(&panel, 16..18, cx),
3010        &["      modified6.txt  <== selected", "      unmodified5.txt",],
3011    );
3012
3013    // Wraps around to first modified file
3014    panel.update_in(cx, |panel, window, cx| {
3015        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3016    });
3017    cx.run_until_parked();
3018
3019    assert_eq!(
3020        visible_entries_as_strings(&panel, 0..18, cx),
3021        &[
3022            "v tree1",
3023            "    > .git",
3024            "    v dir1",
3025            "          modified1.txt  <== selected",
3026            "          modified2.txt",
3027            "          unmodified1.txt",
3028            "    v dir2",
3029            "          modified3.txt",
3030            "          unmodified2.txt",
3031            "      modified4.txt",
3032            "      unmodified3.txt",
3033            "v tree2",
3034            "    > .git",
3035            "    v dir3",
3036            "          modified5.txt",
3037            "          unmodified4.txt",
3038            "      modified6.txt",
3039            "      unmodified5.txt",
3040        ],
3041    );
3042
3043    // Wraps around again to last modified file
3044    panel.update_in(cx, |panel, window, cx| {
3045        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3046    });
3047    cx.run_until_parked();
3048
3049    assert_eq!(
3050        visible_entries_as_strings(&panel, 16..18, cx),
3051        &["      modified6.txt  <== selected", "      unmodified5.txt",],
3052    );
3053
3054    panel.update_in(cx, |panel, window, cx| {
3055        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3056    });
3057    cx.run_until_parked();
3058
3059    assert_eq!(
3060        visible_entries_as_strings(&panel, 13..16, cx),
3061        &[
3062            "    v dir3",
3063            "          modified5.txt  <== selected",
3064            "          unmodified4.txt",
3065        ],
3066    );
3067
3068    panel.update_in(cx, |panel, window, cx| {
3069        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3070    });
3071    cx.run_until_parked();
3072
3073    assert_eq!(
3074        visible_entries_as_strings(&panel, 9..11, cx),
3075        &["      modified4.txt  <== selected", "      unmodified3.txt",],
3076    );
3077
3078    panel.update_in(cx, |panel, window, cx| {
3079        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3080    });
3081    cx.run_until_parked();
3082
3083    assert_eq!(
3084        visible_entries_as_strings(&panel, 6..9, cx),
3085        &[
3086            "    v dir2",
3087            "          modified3.txt  <== selected",
3088            "          unmodified2.txt",
3089        ],
3090    );
3091
3092    panel.update_in(cx, |panel, window, cx| {
3093        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3094    });
3095    cx.run_until_parked();
3096
3097    assert_eq!(
3098        visible_entries_as_strings(&panel, 0..6, cx),
3099        &[
3100            "v tree1",
3101            "    > .git",
3102            "    v dir1",
3103            "          modified1.txt",
3104            "          modified2.txt  <== selected",
3105            "          unmodified1.txt",
3106        ],
3107    );
3108
3109    panel.update_in(cx, |panel, window, cx| {
3110        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3111    });
3112    cx.run_until_parked();
3113
3114    assert_eq!(
3115        visible_entries_as_strings(&panel, 0..6, cx),
3116        &[
3117            "v tree1",
3118            "    > .git",
3119            "    v dir1",
3120            "          modified1.txt  <== selected",
3121            "          modified2.txt",
3122            "          unmodified1.txt",
3123        ],
3124    );
3125}
3126
3127#[gpui::test]
3128async fn test_select_directory(cx: &mut gpui::TestAppContext) {
3129    init_test_with_editor(cx);
3130
3131    let fs = FakeFs::new(cx.executor());
3132    fs.insert_tree(
3133        "/project_root",
3134        json!({
3135            "dir_1": {
3136                "nested_dir": {
3137                    "file_a.py": "# File contents",
3138                }
3139            },
3140            "file_1.py": "# File contents",
3141            "dir_2": {
3142
3143            },
3144            "dir_3": {
3145
3146            },
3147            "file_2.py": "# File contents",
3148            "dir_4": {
3149
3150            },
3151        }),
3152    )
3153    .await;
3154
3155    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3156    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3157    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3158    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3159    cx.run_until_parked();
3160
3161    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3162    cx.executor().run_until_parked();
3163    select_path(&panel, "project_root/dir_1", cx);
3164    cx.executor().run_until_parked();
3165    assert_eq!(
3166        visible_entries_as_strings(&panel, 0..10, cx),
3167        &[
3168            "v project_root",
3169            "    > dir_1  <== selected",
3170            "    > dir_2",
3171            "    > dir_3",
3172            "    > dir_4",
3173            "      file_1.py",
3174            "      file_2.py",
3175        ]
3176    );
3177    panel.update_in(cx, |panel, window, cx| {
3178        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3179    });
3180
3181    assert_eq!(
3182        visible_entries_as_strings(&panel, 0..10, cx),
3183        &[
3184            "v project_root  <== selected",
3185            "    > dir_1",
3186            "    > dir_2",
3187            "    > dir_3",
3188            "    > dir_4",
3189            "      file_1.py",
3190            "      file_2.py",
3191        ]
3192    );
3193
3194    panel.update_in(cx, |panel, window, cx| {
3195        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3196    });
3197
3198    assert_eq!(
3199        visible_entries_as_strings(&panel, 0..10, cx),
3200        &[
3201            "v project_root",
3202            "    > dir_1",
3203            "    > dir_2",
3204            "    > dir_3",
3205            "    > dir_4  <== selected",
3206            "      file_1.py",
3207            "      file_2.py",
3208        ]
3209    );
3210
3211    panel.update_in(cx, |panel, window, cx| {
3212        panel.select_next_directory(&SelectNextDirectory, window, cx)
3213    });
3214
3215    assert_eq!(
3216        visible_entries_as_strings(&panel, 0..10, cx),
3217        &[
3218            "v project_root  <== selected",
3219            "    > dir_1",
3220            "    > dir_2",
3221            "    > dir_3",
3222            "    > dir_4",
3223            "      file_1.py",
3224            "      file_2.py",
3225        ]
3226    );
3227}
3228
3229#[gpui::test]
3230async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
3231    init_test_with_editor(cx);
3232
3233    let fs = FakeFs::new(cx.executor());
3234    fs.insert_tree(
3235        "/project_root",
3236        json!({
3237            "dir_1": {
3238                "nested_dir": {
3239                    "file_a.py": "# File contents",
3240                }
3241            },
3242            "file_1.py": "# File contents",
3243            "file_2.py": "# File contents",
3244            "zdir_2": {
3245                "nested_dir2": {
3246                    "file_b.py": "# File contents",
3247                }
3248            },
3249        }),
3250    )
3251    .await;
3252
3253    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3254    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3255    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3256    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3257    cx.run_until_parked();
3258
3259    assert_eq!(
3260        visible_entries_as_strings(&panel, 0..10, cx),
3261        &[
3262            "v project_root",
3263            "    > dir_1",
3264            "    > zdir_2",
3265            "      file_1.py",
3266            "      file_2.py",
3267        ]
3268    );
3269    panel.update_in(cx, |panel, window, cx| {
3270        panel.select_first(&SelectFirst, window, cx)
3271    });
3272
3273    assert_eq!(
3274        visible_entries_as_strings(&panel, 0..10, cx),
3275        &[
3276            "v project_root  <== selected",
3277            "    > dir_1",
3278            "    > zdir_2",
3279            "      file_1.py",
3280            "      file_2.py",
3281        ]
3282    );
3283
3284    panel.update_in(cx, |panel, window, cx| {
3285        panel.select_last(&SelectLast, window, cx)
3286    });
3287
3288    assert_eq!(
3289        visible_entries_as_strings(&panel, 0..10, cx),
3290        &[
3291            "v project_root",
3292            "    > dir_1",
3293            "    > zdir_2",
3294            "      file_1.py",
3295            "      file_2.py  <== selected",
3296        ]
3297    );
3298
3299    cx.update(|_, cx| {
3300        let settings = *ProjectPanelSettings::get_global(cx);
3301        ProjectPanelSettings::override_global(
3302            ProjectPanelSettings {
3303                hide_root: true,
3304                ..settings
3305            },
3306            cx,
3307        );
3308    });
3309
3310    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3311    cx.run_until_parked();
3312
3313    #[rustfmt::skip]
3314    assert_eq!(
3315        visible_entries_as_strings(&panel, 0..10, cx),
3316        &[
3317            "> dir_1",
3318            "> zdir_2",
3319            "  file_1.py",
3320            "  file_2.py",
3321        ],
3322        "With hide_root=true, root should be hidden"
3323    );
3324
3325    panel.update_in(cx, |panel, window, cx| {
3326        panel.select_first(&SelectFirst, window, cx)
3327    });
3328
3329    assert_eq!(
3330        visible_entries_as_strings(&panel, 0..10, cx),
3331        &[
3332            "> dir_1  <== selected",
3333            "> zdir_2",
3334            "  file_1.py",
3335            "  file_2.py",
3336        ],
3337        "With hide_root=true, first entry should be dir_1, not the hidden root"
3338    );
3339}
3340
3341#[gpui::test]
3342async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
3343    init_test_with_editor(cx);
3344
3345    let fs = FakeFs::new(cx.executor());
3346    fs.insert_tree(
3347        "/project_root",
3348        json!({
3349            "dir_1": {
3350                "nested_dir": {
3351                    "file_a.py": "# File contents",
3352                }
3353            },
3354            "file_1.py": "# File contents",
3355        }),
3356    )
3357    .await;
3358
3359    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3360    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3361    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3362    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3363    cx.run_until_parked();
3364
3365    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3366    cx.executor().run_until_parked();
3367    select_path(&panel, "project_root/dir_1", cx);
3368    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3369    select_path(&panel, "project_root/dir_1/nested_dir", cx);
3370    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3371    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3372    cx.executor().run_until_parked();
3373    assert_eq!(
3374        visible_entries_as_strings(&panel, 0..10, cx),
3375        &[
3376            "v project_root",
3377            "    v dir_1",
3378            "        > nested_dir  <== selected",
3379            "      file_1.py",
3380        ]
3381    );
3382}
3383
3384#[gpui::test]
3385async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
3386    init_test_with_editor(cx);
3387
3388    let fs = FakeFs::new(cx.executor());
3389    fs.insert_tree(
3390        "/project_root",
3391        json!({
3392            "dir_1": {
3393                "nested_dir": {
3394                    "file_a.py": "# File contents",
3395                    "file_b.py": "# File contents",
3396                    "file_c.py": "# File contents",
3397                },
3398                "file_1.py": "# File contents",
3399                "file_2.py": "# File contents",
3400                "file_3.py": "# File contents",
3401            },
3402            "dir_2": {
3403                "file_1.py": "# File contents",
3404                "file_2.py": "# File contents",
3405                "file_3.py": "# File contents",
3406            }
3407        }),
3408    )
3409    .await;
3410
3411    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3412    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3413    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3414    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3415    cx.run_until_parked();
3416
3417    panel.update_in(cx, |panel, window, cx| {
3418        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3419    });
3420    cx.executor().run_until_parked();
3421    assert_eq!(
3422        visible_entries_as_strings(&panel, 0..10, cx),
3423        &["v project_root", "    > dir_1", "    > dir_2",]
3424    );
3425
3426    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
3427    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3428    cx.executor().run_until_parked();
3429    assert_eq!(
3430        visible_entries_as_strings(&panel, 0..10, cx),
3431        &[
3432            "v project_root",
3433            "    v dir_1  <== selected",
3434            "        > nested_dir",
3435            "          file_1.py",
3436            "          file_2.py",
3437            "          file_3.py",
3438            "    > dir_2",
3439        ]
3440    );
3441}
3442
3443#[gpui::test]
3444async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
3445    init_test_with_editor(cx);
3446
3447    let fs = FakeFs::new(cx.executor());
3448    let worktree_content = json!({
3449        "dir_1": {
3450            "file_1.py": "# File contents",
3451        },
3452        "dir_2": {
3453            "file_1.py": "# File contents",
3454        }
3455    });
3456
3457    fs.insert_tree("/project_root_1", worktree_content.clone())
3458        .await;
3459    fs.insert_tree("/project_root_2", worktree_content).await;
3460
3461    let project = Project::test(
3462        fs.clone(),
3463        ["/project_root_1".as_ref(), "/project_root_2".as_ref()],
3464        cx,
3465    )
3466    .await;
3467    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3468    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3469    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3470    cx.run_until_parked();
3471
3472    panel.update_in(cx, |panel, window, cx| {
3473        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3474    });
3475    cx.executor().run_until_parked();
3476    assert_eq!(
3477        visible_entries_as_strings(&panel, 0..10, cx),
3478        &["> project_root_1", "> project_root_2",]
3479    );
3480}
3481
3482#[gpui::test]
3483async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) {
3484    init_test_with_editor(cx);
3485
3486    let fs = FakeFs::new(cx.executor());
3487    fs.insert_tree(
3488        "/project_root",
3489        json!({
3490            "dir_1": {
3491                "nested_dir": {
3492                    "file_a.py": "# File contents",
3493                    "file_b.py": "# File contents",
3494                    "file_c.py": "# File contents",
3495                },
3496                "file_1.py": "# File contents",
3497                "file_2.py": "# File contents",
3498                "file_3.py": "# File contents",
3499            },
3500            "dir_2": {
3501                "file_1.py": "# File contents",
3502                "file_2.py": "# File contents",
3503                "file_3.py": "# File contents",
3504            }
3505        }),
3506    )
3507    .await;
3508
3509    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3510    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3511    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3512    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3513    cx.run_until_parked();
3514
3515    // Open project_root/dir_1 to ensure that a nested directory is expanded
3516    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3517    cx.executor().run_until_parked();
3518    assert_eq!(
3519        visible_entries_as_strings(&panel, 0..10, cx),
3520        &[
3521            "v project_root",
3522            "    v dir_1  <== selected",
3523            "        > nested_dir",
3524            "          file_1.py",
3525            "          file_2.py",
3526            "          file_3.py",
3527            "    > dir_2",
3528        ]
3529    );
3530
3531    // Close root directory
3532    toggle_expand_dir(&panel, "project_root", cx);
3533    cx.executor().run_until_parked();
3534    assert_eq!(
3535        visible_entries_as_strings(&panel, 0..10, cx),
3536        &["> project_root  <== selected"]
3537    );
3538
3539    // Run collapse_all_entries and make sure root is not expanded
3540    panel.update_in(cx, |panel, window, cx| {
3541        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3542    });
3543    cx.executor().run_until_parked();
3544    assert_eq!(
3545        visible_entries_as_strings(&panel, 0..10, cx),
3546        &["> project_root  <== selected"]
3547    );
3548}
3549
3550#[gpui::test]
3551async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
3552    init_test(cx);
3553
3554    let fs = FakeFs::new(cx.executor());
3555    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
3556    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
3557    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3558    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3559    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3560    cx.run_until_parked();
3561
3562    // Make a new buffer with no backing file
3563    workspace
3564        .update(cx, |workspace, window, cx| {
3565            Editor::new_file(workspace, &Default::default(), window, cx)
3566        })
3567        .unwrap();
3568
3569    cx.executor().run_until_parked();
3570
3571    // "Save as" the buffer, creating a new backing file for it
3572    let save_task = workspace
3573        .update(cx, |workspace, window, cx| {
3574            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3575        })
3576        .unwrap();
3577
3578    cx.executor().run_until_parked();
3579    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
3580    save_task.await.unwrap();
3581
3582    // Rename the file
3583    select_path(&panel, "root/new", cx);
3584    assert_eq!(
3585        visible_entries_as_strings(&panel, 0..10, cx),
3586        &["v root", "      new  <== selected  <== marked"]
3587    );
3588    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3589    panel.update_in(cx, |panel, window, cx| {
3590        panel
3591            .filename_editor
3592            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
3593    });
3594    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3595
3596    cx.executor().run_until_parked();
3597    assert_eq!(
3598        visible_entries_as_strings(&panel, 0..10, cx),
3599        &["v root", "      newer  <== selected"]
3600    );
3601
3602    workspace
3603        .update(cx, |workspace, window, cx| {
3604            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3605        })
3606        .unwrap()
3607        .await
3608        .unwrap();
3609
3610    cx.executor().run_until_parked();
3611    // assert that saving the file doesn't restore "new"
3612    assert_eq!(
3613        visible_entries_as_strings(&panel, 0..10, cx),
3614        &["v root", "      newer  <== selected"]
3615    );
3616}
3617
3618// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
3619// you can't rename a directory which some program has already open. This is a
3620// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
3621// to it, and thus renaming it will fail on Windows.
3622// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
3623// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
3624#[gpui::test]
3625#[cfg_attr(target_os = "windows", ignore)]
3626async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
3627    init_test_with_editor(cx);
3628
3629    let fs = FakeFs::new(cx.executor());
3630    fs.insert_tree(
3631        "/root1",
3632        json!({
3633            "dir1": {
3634                "file1.txt": "content 1",
3635            },
3636        }),
3637    )
3638    .await;
3639
3640    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3641    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3642    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3643    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3644    cx.run_until_parked();
3645
3646    toggle_expand_dir(&panel, "root1/dir1", cx);
3647
3648    assert_eq!(
3649        visible_entries_as_strings(&panel, 0..20, cx),
3650        &["v root1", "    v dir1  <== selected", "          file1.txt",],
3651        "Initial state with worktrees"
3652    );
3653
3654    select_path(&panel, "root1", cx);
3655    assert_eq!(
3656        visible_entries_as_strings(&panel, 0..20, cx),
3657        &["v root1  <== selected", "    v dir1", "          file1.txt",],
3658    );
3659
3660    // Rename root1 to new_root1
3661    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3662
3663    assert_eq!(
3664        visible_entries_as_strings(&panel, 0..20, cx),
3665        &[
3666            "v [EDITOR: 'root1']  <== selected",
3667            "    v dir1",
3668            "          file1.txt",
3669        ],
3670    );
3671
3672    let confirm = panel.update_in(cx, |panel, window, cx| {
3673        panel
3674            .filename_editor
3675            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
3676        panel.confirm_edit(true, window, cx).unwrap()
3677    });
3678    confirm.await.unwrap();
3679    cx.run_until_parked();
3680    assert_eq!(
3681        visible_entries_as_strings(&panel, 0..20, cx),
3682        &[
3683            "v new_root1  <== selected",
3684            "    v dir1",
3685            "          file1.txt",
3686        ],
3687        "Should update worktree name"
3688    );
3689
3690    // Ensure internal paths have been updated
3691    select_path(&panel, "new_root1/dir1/file1.txt", cx);
3692    assert_eq!(
3693        visible_entries_as_strings(&panel, 0..20, cx),
3694        &[
3695            "v new_root1",
3696            "    v dir1",
3697            "          file1.txt  <== selected",
3698        ],
3699        "Files in renamed worktree are selectable"
3700    );
3701}
3702
3703#[gpui::test]
3704async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
3705    init_test_with_editor(cx);
3706
3707    let fs = FakeFs::new(cx.executor());
3708    fs.insert_tree(
3709        "/root1",
3710        json!({
3711            "dir1": { "file1.txt": "content" },
3712            "file2.txt": "content",
3713        }),
3714    )
3715    .await;
3716    fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
3717        .await;
3718
3719    // Test 1: Single worktree, hide_root=true - rename should be blocked
3720    {
3721        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3722        let workspace =
3723            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3724        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3725
3726        cx.update(|_, cx| {
3727            let settings = *ProjectPanelSettings::get_global(cx);
3728            ProjectPanelSettings::override_global(
3729                ProjectPanelSettings {
3730                    hide_root: true,
3731                    ..settings
3732                },
3733                cx,
3734            );
3735        });
3736
3737        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3738        cx.run_until_parked();
3739
3740        panel.update(cx, |panel, cx| {
3741            let project = panel.project.read(cx);
3742            let worktree = project.visible_worktrees(cx).next().unwrap();
3743            let root_entry = worktree.read(cx).root_entry().unwrap();
3744            panel.state.selection = Some(SelectedEntry {
3745                worktree_id: worktree.read(cx).id(),
3746                entry_id: root_entry.id,
3747            });
3748        });
3749
3750        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3751
3752        assert!(
3753            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3754            "Rename should be blocked when hide_root=true with single worktree"
3755        );
3756    }
3757
3758    // Test 2: Multiple worktrees, hide_root=true - rename should work
3759    {
3760        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3761        let workspace =
3762            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3763        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3764
3765        cx.update(|_, cx| {
3766            let settings = *ProjectPanelSettings::get_global(cx);
3767            ProjectPanelSettings::override_global(
3768                ProjectPanelSettings {
3769                    hide_root: true,
3770                    ..settings
3771                },
3772                cx,
3773            );
3774        });
3775
3776        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3777        cx.run_until_parked();
3778
3779        select_path(&panel, "root1", cx);
3780        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3781
3782        #[cfg(target_os = "windows")]
3783        assert!(
3784            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3785            "Rename should be blocked on Windows even with multiple worktrees"
3786        );
3787
3788        #[cfg(not(target_os = "windows"))]
3789        {
3790            assert!(
3791                panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()),
3792                "Rename should work with multiple worktrees on non-Windows when hide_root=true"
3793            );
3794            panel.update_in(cx, |panel, window, cx| {
3795                panel.cancel(&menu::Cancel, window, cx)
3796            });
3797        }
3798    }
3799}
3800
3801#[gpui::test]
3802async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
3803    init_test_with_editor(cx);
3804    let fs = FakeFs::new(cx.executor());
3805    fs.insert_tree(
3806        "/project_root",
3807        json!({
3808            "dir_1": {
3809                "nested_dir": {
3810                    "file_a.py": "# File contents",
3811                }
3812            },
3813            "file_1.py": "# File contents",
3814        }),
3815    )
3816    .await;
3817
3818    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3819    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
3820    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3821    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3822    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3823    cx.run_until_parked();
3824
3825    cx.update(|window, cx| {
3826        panel.update(cx, |this, cx| {
3827            this.select_next(&Default::default(), window, cx);
3828            this.expand_selected_entry(&Default::default(), window, cx);
3829        })
3830    });
3831    cx.run_until_parked();
3832
3833    cx.update(|window, cx| {
3834        panel.update(cx, |this, cx| {
3835            this.expand_selected_entry(&Default::default(), window, cx);
3836        })
3837    });
3838    cx.run_until_parked();
3839
3840    cx.update(|window, cx| {
3841        panel.update(cx, |this, cx| {
3842            this.select_next(&Default::default(), window, cx);
3843            this.expand_selected_entry(&Default::default(), window, cx);
3844        })
3845    });
3846    cx.run_until_parked();
3847
3848    cx.update(|window, cx| {
3849        panel.update(cx, |this, cx| {
3850            this.select_next(&Default::default(), window, cx);
3851        })
3852    });
3853    cx.run_until_parked();
3854
3855    assert_eq!(
3856        visible_entries_as_strings(&panel, 0..10, cx),
3857        &[
3858            "v project_root",
3859            "    v dir_1",
3860            "        v nested_dir",
3861            "              file_a.py  <== selected",
3862            "      file_1.py",
3863        ]
3864    );
3865    let modifiers_with_shift = gpui::Modifiers {
3866        shift: true,
3867        ..Default::default()
3868    };
3869    cx.run_until_parked();
3870    cx.simulate_modifiers_change(modifiers_with_shift);
3871    cx.update(|window, cx| {
3872        panel.update(cx, |this, cx| {
3873            this.select_next(&Default::default(), window, cx);
3874        })
3875    });
3876    assert_eq!(
3877        visible_entries_as_strings(&panel, 0..10, cx),
3878        &[
3879            "v project_root",
3880            "    v dir_1",
3881            "        v nested_dir",
3882            "              file_a.py",
3883            "      file_1.py  <== selected  <== marked",
3884        ]
3885    );
3886    cx.update(|window, cx| {
3887        panel.update(cx, |this, cx| {
3888            this.select_previous(&Default::default(), window, cx);
3889        })
3890    });
3891    assert_eq!(
3892        visible_entries_as_strings(&panel, 0..10, cx),
3893        &[
3894            "v project_root",
3895            "    v dir_1",
3896            "        v nested_dir",
3897            "              file_a.py  <== selected  <== marked",
3898            "      file_1.py  <== marked",
3899        ]
3900    );
3901    cx.update(|window, cx| {
3902        panel.update(cx, |this, cx| {
3903            let drag = DraggedSelection {
3904                active_selection: this.state.selection.unwrap(),
3905                marked_selections: this.marked_entries.clone().into(),
3906            };
3907            let target_entry = this
3908                .project
3909                .read(cx)
3910                .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
3911                .unwrap();
3912            this.drag_onto(&drag, target_entry.id, false, window, cx);
3913        });
3914    });
3915    cx.run_until_parked();
3916    assert_eq!(
3917        visible_entries_as_strings(&panel, 0..10, cx),
3918        &[
3919            "v project_root",
3920            "    v dir_1",
3921            "        v nested_dir",
3922            "      file_1.py  <== marked",
3923            "      file_a.py  <== selected  <== marked",
3924        ]
3925    );
3926    // ESC clears out all marks
3927    cx.update(|window, cx| {
3928        panel.update(cx, |this, cx| {
3929            this.cancel(&menu::Cancel, window, cx);
3930        })
3931    });
3932    assert_eq!(
3933        visible_entries_as_strings(&panel, 0..10, cx),
3934        &[
3935            "v project_root",
3936            "    v dir_1",
3937            "        v nested_dir",
3938            "      file_1.py",
3939            "      file_a.py  <== selected",
3940        ]
3941    );
3942    // ESC clears out all marks
3943    cx.update(|window, cx| {
3944        panel.update(cx, |this, cx| {
3945            this.select_previous(&SelectPrevious, window, cx);
3946            this.select_next(&SelectNext, window, cx);
3947        })
3948    });
3949    assert_eq!(
3950        visible_entries_as_strings(&panel, 0..10, cx),
3951        &[
3952            "v project_root",
3953            "    v dir_1",
3954            "        v nested_dir",
3955            "      file_1.py  <== marked",
3956            "      file_a.py  <== selected  <== marked",
3957        ]
3958    );
3959    cx.simulate_modifiers_change(Default::default());
3960    cx.update(|window, cx| {
3961        panel.update(cx, |this, cx| {
3962            this.cut(&Cut, window, cx);
3963            this.select_previous(&SelectPrevious, window, cx);
3964            this.select_previous(&SelectPrevious, window, cx);
3965
3966            this.paste(&Paste, window, cx);
3967            this.update_visible_entries(None, None, false, window, cx);
3968        })
3969    });
3970    cx.run_until_parked();
3971    assert_eq!(
3972        visible_entries_as_strings(&panel, 0..10, cx),
3973        &[
3974            "v project_root",
3975            "    v dir_1",
3976            "        v nested_dir",
3977            "              file_1.py  <== marked",
3978            "              file_a.py  <== selected  <== marked",
3979        ]
3980    );
3981    cx.simulate_modifiers_change(modifiers_with_shift);
3982    cx.update(|window, cx| {
3983        panel.update(cx, |this, cx| {
3984            this.expand_selected_entry(&Default::default(), window, cx);
3985            this.select_next(&SelectNext, window, cx);
3986            this.select_next(&SelectNext, window, cx);
3987        })
3988    });
3989    submit_deletion(&panel, cx);
3990    assert_eq!(
3991        visible_entries_as_strings(&panel, 0..10, cx),
3992        &[
3993            "v project_root",
3994            "    v dir_1",
3995            "        v nested_dir  <== selected",
3996        ]
3997    );
3998}
3999
4000#[gpui::test]
4001async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
4002    init_test(cx);
4003
4004    let fs = FakeFs::new(cx.executor());
4005    fs.insert_tree(
4006        "/root",
4007        json!({
4008            "a": {
4009                "b": {
4010                    "c": {
4011                        "d": {}
4012                    }
4013                }
4014            },
4015            "target_destination": {}
4016        }),
4017    )
4018    .await;
4019
4020    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4021    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4022    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4023
4024    cx.update(|_, cx| {
4025        let settings = *ProjectPanelSettings::get_global(cx);
4026        ProjectPanelSettings::override_global(
4027            ProjectPanelSettings {
4028                auto_fold_dirs: true,
4029                ..settings
4030            },
4031            cx,
4032        );
4033    });
4034
4035    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4036    cx.run_until_parked();
4037
4038    // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
4039    select_path(&panel, "root/a/b/c/d", cx);
4040    panel.update_in(cx, |panel, window, cx| {
4041        let drag = DraggedSelection {
4042            active_selection: SelectedEntry {
4043                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4044                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4045            },
4046            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4047        };
4048        let target_entry = panel
4049            .project
4050            .read(cx)
4051            .visible_worktrees(cx)
4052            .next()
4053            .unwrap()
4054            .read(cx)
4055            .entry_for_path(rel_path("target_destination"))
4056            .unwrap();
4057        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4058    });
4059    cx.executor().run_until_parked();
4060
4061    assert_eq!(
4062        visible_entries_as_strings(&panel, 0..10, cx),
4063        &[
4064            "v root",
4065            "    > a/b/c",
4066            "    > target_destination/d  <== selected"
4067        ],
4068        "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
4069    );
4070
4071    // Reset
4072    select_path(&panel, "root/target_destination/d", cx);
4073    panel.update_in(cx, |panel, window, cx| {
4074        let drag = DraggedSelection {
4075            active_selection: SelectedEntry {
4076                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4077                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4078            },
4079            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4080        };
4081        let target_entry = panel
4082            .project
4083            .read(cx)
4084            .visible_worktrees(cx)
4085            .next()
4086            .unwrap()
4087            .read(cx)
4088            .entry_for_path(rel_path("a/b/c"))
4089            .unwrap();
4090        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4091    });
4092    cx.executor().run_until_parked();
4093
4094    // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
4095    select_path(&panel, "root/a/b", cx);
4096    panel.update_in(cx, |panel, window, cx| {
4097        let drag = DraggedSelection {
4098            active_selection: SelectedEntry {
4099                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4100                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4101            },
4102            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4103        };
4104        let target_entry = panel
4105            .project
4106            .read(cx)
4107            .visible_worktrees(cx)
4108            .next()
4109            .unwrap()
4110            .read(cx)
4111            .entry_for_path(rel_path("target_destination"))
4112            .unwrap();
4113        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4114    });
4115    cx.executor().run_until_parked();
4116
4117    assert_eq!(
4118        visible_entries_as_strings(&panel, 0..10, cx),
4119        &["v root", "    v a", "    > target_destination/b/c/d"],
4120        "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
4121    );
4122
4123    // Reset
4124    select_path(&panel, "root/target_destination/b", cx);
4125    panel.update_in(cx, |panel, window, cx| {
4126        let drag = DraggedSelection {
4127            active_selection: SelectedEntry {
4128                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4129                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4130            },
4131            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4132        };
4133        let target_entry = panel
4134            .project
4135            .read(cx)
4136            .visible_worktrees(cx)
4137            .next()
4138            .unwrap()
4139            .read(cx)
4140            .entry_for_path(rel_path("a"))
4141            .unwrap();
4142        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4143    });
4144    cx.executor().run_until_parked();
4145
4146    // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
4147    select_path(&panel, "root/a", cx);
4148    panel.update_in(cx, |panel, window, cx| {
4149        let drag = DraggedSelection {
4150            active_selection: SelectedEntry {
4151                worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4152                entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4153            },
4154            marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4155        };
4156        let target_entry = panel
4157            .project
4158            .read(cx)
4159            .visible_worktrees(cx)
4160            .next()
4161            .unwrap()
4162            .read(cx)
4163            .entry_for_path(rel_path("target_destination"))
4164            .unwrap();
4165        panel.drag_onto(&drag, target_entry.id, false, window, cx);
4166    });
4167    cx.executor().run_until_parked();
4168
4169    assert_eq!(
4170        visible_entries_as_strings(&panel, 0..10, cx),
4171        &["v root", "    > target_destination/a/b/c/d"],
4172        "Moving first directory 'a' should move whole 'a/b/c/d' chain"
4173    );
4174}
4175
4176#[gpui::test]
4177async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4178    init_test(cx);
4179
4180    let fs = FakeFs::new(cx.executor());
4181    fs.insert_tree(
4182        "/root_a",
4183        json!({
4184            "src": {
4185                "lib.rs": "",
4186                "main.rs": ""
4187            },
4188            "docs": {
4189                "guide.md": ""
4190            },
4191            "multi": {
4192                "alpha.txt": "",
4193                "beta.txt": ""
4194            }
4195        }),
4196    )
4197    .await;
4198    fs.insert_tree(
4199        "/root_b",
4200        json!({
4201            "dst": {
4202                "existing.md": ""
4203            },
4204            "target.txt": ""
4205        }),
4206    )
4207    .await;
4208
4209    let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
4210    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4211    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4212    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4213    cx.run_until_parked();
4214
4215    // Case 1: move a file onto a directory in another worktree.
4216    select_path(&panel, "root_a/src/main.rs", cx);
4217    drag_selection_to(&panel, "root_b/dst", false, cx);
4218    assert!(
4219        find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(),
4220        "Dragged file should appear under destination worktree"
4221    );
4222    assert_eq!(
4223        find_project_entry(&panel, "root_a/src/main.rs", cx),
4224        None,
4225        "Dragged file should be removed from the source worktree"
4226    );
4227
4228    // Case 2: drop a file onto another worktree file so it lands in the parent directory.
4229    select_path(&panel, "root_a/docs/guide.md", cx);
4230    drag_selection_to(&panel, "root_b/dst/existing.md", true, cx);
4231    assert!(
4232        find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(),
4233        "Dropping onto a file should place the entry beside the target file"
4234    );
4235    assert_eq!(
4236        find_project_entry(&panel, "root_a/docs/guide.md", cx),
4237        None,
4238        "Source file should be removed after the move"
4239    );
4240
4241    // Case 3: move an entire directory.
4242    select_path(&panel, "root_a/src", cx);
4243    drag_selection_to(&panel, "root_b/dst", false, cx);
4244    assert!(
4245        find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(),
4246        "Dragging a directory should move its nested contents"
4247    );
4248    assert_eq!(
4249        find_project_entry(&panel, "root_a/src", cx),
4250        None,
4251        "Directory should no longer exist in the source worktree"
4252    );
4253
4254    // Case 4: multi-selection drag between worktrees.
4255    panel.update(cx, |panel, _| panel.marked_entries.clear());
4256    select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx);
4257    select_path_with_mark(&panel, "root_a/multi/beta.txt", cx);
4258    drag_selection_to(&panel, "root_b/dst", false, cx);
4259    assert!(
4260        find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some()
4261            && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(),
4262        "All marked entries should move to the destination worktree"
4263    );
4264    assert_eq!(
4265        find_project_entry(&panel, "root_a/multi/alpha.txt", cx),
4266        None,
4267        "Marked entries should be removed from the origin worktree"
4268    );
4269    assert_eq!(
4270        find_project_entry(&panel, "root_a/multi/beta.txt", cx),
4271        None,
4272        "Marked entries should be removed from the origin worktree"
4273    );
4274}
4275
4276#[gpui::test]
4277async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
4278    init_test_with_editor(cx);
4279    cx.update(|cx| {
4280        cx.update_global::<SettingsStore, _>(|store, cx| {
4281            store.update_user_settings(cx, |settings| {
4282                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4283                settings
4284                    .project_panel
4285                    .get_or_insert_default()
4286                    .auto_reveal_entries = Some(false);
4287            });
4288        })
4289    });
4290
4291    let fs = FakeFs::new(cx.background_executor.clone());
4292    fs.insert_tree(
4293        "/project_root",
4294        json!({
4295            ".git": {},
4296            ".gitignore": "**/gitignored_dir",
4297            "dir_1": {
4298                "file_1.py": "# File 1_1 contents",
4299                "file_2.py": "# File 1_2 contents",
4300                "file_3.py": "# File 1_3 contents",
4301                "gitignored_dir": {
4302                    "file_a.py": "# File contents",
4303                    "file_b.py": "# File contents",
4304                    "file_c.py": "# File contents",
4305                },
4306            },
4307            "dir_2": {
4308                "file_1.py": "# File 2_1 contents",
4309                "file_2.py": "# File 2_2 contents",
4310                "file_3.py": "# File 2_3 contents",
4311            }
4312        }),
4313    )
4314    .await;
4315
4316    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4317    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4318    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4319    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4320    cx.run_until_parked();
4321
4322    assert_eq!(
4323        visible_entries_as_strings(&panel, 0..20, cx),
4324        &[
4325            "v project_root",
4326            "    > .git",
4327            "    > dir_1",
4328            "    > dir_2",
4329            "      .gitignore",
4330        ]
4331    );
4332
4333    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4334        .expect("dir 1 file is not ignored and should have an entry");
4335    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4336        .expect("dir 2 file is not ignored and should have an entry");
4337    let gitignored_dir_file =
4338        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4339    assert_eq!(
4340        gitignored_dir_file, None,
4341        "File in the gitignored dir should not have an entry before its dir is toggled"
4342    );
4343
4344    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4345    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4346    cx.executor().run_until_parked();
4347    assert_eq!(
4348        visible_entries_as_strings(&panel, 0..20, cx),
4349        &[
4350            "v project_root",
4351            "    > .git",
4352            "    v dir_1",
4353            "        v gitignored_dir  <== selected",
4354            "              file_a.py",
4355            "              file_b.py",
4356            "              file_c.py",
4357            "          file_1.py",
4358            "          file_2.py",
4359            "          file_3.py",
4360            "    > dir_2",
4361            "      .gitignore",
4362        ],
4363        "Should show gitignored dir file list in the project panel"
4364    );
4365    let gitignored_dir_file =
4366        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4367            .expect("after gitignored dir got opened, a file entry should be present");
4368
4369    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4370    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4371    assert_eq!(
4372        visible_entries_as_strings(&panel, 0..20, cx),
4373        &[
4374            "v project_root",
4375            "    > .git",
4376            "    > dir_1  <== selected",
4377            "    > dir_2",
4378            "      .gitignore",
4379        ],
4380        "Should hide all dir contents again and prepare for the auto reveal test"
4381    );
4382
4383    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4384        panel.update(cx, |panel, cx| {
4385            panel.project.update(cx, |_, cx| {
4386                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4387            })
4388        });
4389        cx.run_until_parked();
4390        assert_eq!(
4391            visible_entries_as_strings(&panel, 0..20, cx),
4392            &[
4393                "v project_root",
4394                "    > .git",
4395                "    > dir_1  <== selected",
4396                "    > dir_2",
4397                "      .gitignore",
4398            ],
4399            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4400        );
4401    }
4402
4403    cx.update(|_, cx| {
4404        cx.update_global::<SettingsStore, _>(|store, cx| {
4405            store.update_user_settings(cx, |settings| {
4406                settings
4407                    .project_panel
4408                    .get_or_insert_default()
4409                    .auto_reveal_entries = Some(true)
4410            });
4411        })
4412    });
4413
4414    panel.update(cx, |panel, cx| {
4415        panel.project.update(cx, |_, cx| {
4416            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
4417        })
4418    });
4419    cx.run_until_parked();
4420    assert_eq!(
4421        visible_entries_as_strings(&panel, 0..20, cx),
4422        &[
4423            "v project_root",
4424            "    > .git",
4425            "    v dir_1",
4426            "        > gitignored_dir",
4427            "          file_1.py  <== selected  <== marked",
4428            "          file_2.py",
4429            "          file_3.py",
4430            "    > dir_2",
4431            "      .gitignore",
4432        ],
4433        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
4434    );
4435
4436    panel.update(cx, |panel, cx| {
4437        panel.project.update(cx, |_, cx| {
4438            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
4439        })
4440    });
4441    cx.run_until_parked();
4442    assert_eq!(
4443        visible_entries_as_strings(&panel, 0..20, cx),
4444        &[
4445            "v project_root",
4446            "    > .git",
4447            "    v dir_1",
4448            "        > gitignored_dir",
4449            "          file_1.py",
4450            "          file_2.py",
4451            "          file_3.py",
4452            "    v dir_2",
4453            "          file_1.py  <== selected  <== marked",
4454            "          file_2.py",
4455            "          file_3.py",
4456            "      .gitignore",
4457        ],
4458        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
4459    );
4460
4461    panel.update(cx, |panel, cx| {
4462        panel.project.update(cx, |_, cx| {
4463            cx.emit(project::Event::ActiveEntryChanged(Some(
4464                gitignored_dir_file,
4465            )))
4466        })
4467    });
4468    cx.run_until_parked();
4469    assert_eq!(
4470        visible_entries_as_strings(&panel, 0..20, cx),
4471        &[
4472            "v project_root",
4473            "    > .git",
4474            "    v dir_1",
4475            "        > gitignored_dir",
4476            "          file_1.py",
4477            "          file_2.py",
4478            "          file_3.py",
4479            "    v dir_2",
4480            "          file_1.py  <== selected  <== marked",
4481            "          file_2.py",
4482            "          file_3.py",
4483            "      .gitignore",
4484        ],
4485        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
4486    );
4487
4488    panel.update(cx, |panel, cx| {
4489        panel.project.update(cx, |_, cx| {
4490            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4491        })
4492    });
4493    cx.run_until_parked();
4494    assert_eq!(
4495        visible_entries_as_strings(&panel, 0..20, cx),
4496        &[
4497            "v project_root",
4498            "    > .git",
4499            "    v dir_1",
4500            "        v gitignored_dir",
4501            "              file_a.py  <== selected  <== marked",
4502            "              file_b.py",
4503            "              file_c.py",
4504            "          file_1.py",
4505            "          file_2.py",
4506            "          file_3.py",
4507            "    v dir_2",
4508            "          file_1.py",
4509            "          file_2.py",
4510            "          file_3.py",
4511            "      .gitignore",
4512        ],
4513        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
4514    );
4515}
4516
4517#[gpui::test]
4518async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
4519    init_test_with_editor(cx);
4520    cx.update(|cx| {
4521        cx.update_global::<SettingsStore, _>(|store, cx| {
4522            store.update_user_settings(cx, |settings| {
4523                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4524                settings.project.worktree.file_scan_inclusions =
4525                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
4526                settings
4527                    .project_panel
4528                    .get_or_insert_default()
4529                    .auto_reveal_entries = Some(false)
4530            });
4531        })
4532    });
4533
4534    let fs = FakeFs::new(cx.background_executor.clone());
4535    fs.insert_tree(
4536        "/project_root",
4537        json!({
4538            ".git": {},
4539            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
4540            "dir_1": {
4541                "file_1.py": "# File 1_1 contents",
4542                "file_2.py": "# File 1_2 contents",
4543                "file_3.py": "# File 1_3 contents",
4544                "gitignored_dir": {
4545                    "file_a.py": "# File contents",
4546                    "file_b.py": "# File contents",
4547                    "file_c.py": "# File contents",
4548                },
4549            },
4550            "dir_2": {
4551                "file_1.py": "# File 2_1 contents",
4552                "file_2.py": "# File 2_2 contents",
4553                "file_3.py": "# File 2_3 contents",
4554            },
4555            "always_included_but_ignored_dir": {
4556                "file_a.py": "# File contents",
4557                "file_b.py": "# File contents",
4558                "file_c.py": "# File contents",
4559            },
4560        }),
4561    )
4562    .await;
4563
4564    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4565    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4566    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4567    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4568    cx.run_until_parked();
4569
4570    assert_eq!(
4571        visible_entries_as_strings(&panel, 0..20, cx),
4572        &[
4573            "v project_root",
4574            "    > .git",
4575            "    > always_included_but_ignored_dir",
4576            "    > dir_1",
4577            "    > dir_2",
4578            "      .gitignore",
4579        ]
4580    );
4581
4582    let gitignored_dir_file =
4583        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4584    let always_included_but_ignored_dir_file = find_project_entry(
4585        &panel,
4586        "project_root/always_included_but_ignored_dir/file_a.py",
4587        cx,
4588    )
4589    .expect("file that is .gitignored but set to always be included should have an entry");
4590    assert_eq!(
4591        gitignored_dir_file, None,
4592        "File in the gitignored dir should not have an entry unless its directory is toggled"
4593    );
4594
4595    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4596    cx.run_until_parked();
4597    cx.update(|_, cx| {
4598        cx.update_global::<SettingsStore, _>(|store, cx| {
4599            store.update_user_settings(cx, |settings| {
4600                settings
4601                    .project_panel
4602                    .get_or_insert_default()
4603                    .auto_reveal_entries = Some(true)
4604            });
4605        })
4606    });
4607
4608    panel.update(cx, |panel, cx| {
4609        panel.project.update(cx, |_, cx| {
4610            cx.emit(project::Event::ActiveEntryChanged(Some(
4611                always_included_but_ignored_dir_file,
4612            )))
4613        })
4614    });
4615    cx.run_until_parked();
4616
4617    assert_eq!(
4618        visible_entries_as_strings(&panel, 0..20, cx),
4619        &[
4620            "v project_root",
4621            "    > .git",
4622            "    v always_included_but_ignored_dir",
4623            "          file_a.py  <== selected  <== marked",
4624            "          file_b.py",
4625            "          file_c.py",
4626            "    v dir_1",
4627            "        > gitignored_dir",
4628            "          file_1.py",
4629            "          file_2.py",
4630            "          file_3.py",
4631            "    > dir_2",
4632            "      .gitignore",
4633        ],
4634        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
4635    );
4636}
4637
4638#[gpui::test]
4639async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
4640    init_test_with_editor(cx);
4641    cx.update(|cx| {
4642        cx.update_global::<SettingsStore, _>(|store, cx| {
4643            store.update_user_settings(cx, |settings| {
4644                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4645                settings
4646                    .project_panel
4647                    .get_or_insert_default()
4648                    .auto_reveal_entries = Some(false)
4649            });
4650        })
4651    });
4652
4653    let fs = FakeFs::new(cx.background_executor.clone());
4654    fs.insert_tree(
4655        "/project_root",
4656        json!({
4657            ".git": {},
4658            ".gitignore": "**/gitignored_dir",
4659            "dir_1": {
4660                "file_1.py": "# File 1_1 contents",
4661                "file_2.py": "# File 1_2 contents",
4662                "file_3.py": "# File 1_3 contents",
4663                "gitignored_dir": {
4664                    "file_a.py": "# File contents",
4665                    "file_b.py": "# File contents",
4666                    "file_c.py": "# File contents",
4667                },
4668            },
4669            "dir_2": {
4670                "file_1.py": "# File 2_1 contents",
4671                "file_2.py": "# File 2_2 contents",
4672                "file_3.py": "# File 2_3 contents",
4673            }
4674        }),
4675    )
4676    .await;
4677
4678    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4679    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4680    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4681    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4682    cx.run_until_parked();
4683
4684    assert_eq!(
4685        visible_entries_as_strings(&panel, 0..20, cx),
4686        &[
4687            "v project_root",
4688            "    > .git",
4689            "    > dir_1",
4690            "    > dir_2",
4691            "      .gitignore",
4692        ]
4693    );
4694
4695    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4696        .expect("dir 1 file is not ignored and should have an entry");
4697    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4698        .expect("dir 2 file is not ignored and should have an entry");
4699    let gitignored_dir_file =
4700        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4701    assert_eq!(
4702        gitignored_dir_file, None,
4703        "File in the gitignored dir should not have an entry before its dir is toggled"
4704    );
4705
4706    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4707    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4708    cx.run_until_parked();
4709    assert_eq!(
4710        visible_entries_as_strings(&panel, 0..20, cx),
4711        &[
4712            "v project_root",
4713            "    > .git",
4714            "    v dir_1",
4715            "        v gitignored_dir  <== selected",
4716            "              file_a.py",
4717            "              file_b.py",
4718            "              file_c.py",
4719            "          file_1.py",
4720            "          file_2.py",
4721            "          file_3.py",
4722            "    > dir_2",
4723            "      .gitignore",
4724        ],
4725        "Should show gitignored dir file list in the project panel"
4726    );
4727    let gitignored_dir_file =
4728        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4729            .expect("after gitignored dir got opened, a file entry should be present");
4730
4731    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4732    toggle_expand_dir(&panel, "project_root/dir_1", cx);
4733    assert_eq!(
4734        visible_entries_as_strings(&panel, 0..20, cx),
4735        &[
4736            "v project_root",
4737            "    > .git",
4738            "    > dir_1  <== selected",
4739            "    > dir_2",
4740            "      .gitignore",
4741        ],
4742        "Should hide all dir contents again and prepare for the explicit reveal test"
4743    );
4744
4745    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4746        panel.update(cx, |panel, cx| {
4747            panel.project.update(cx, |_, cx| {
4748                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4749            })
4750        });
4751        cx.run_until_parked();
4752        assert_eq!(
4753            visible_entries_as_strings(&panel, 0..20, cx),
4754            &[
4755                "v project_root",
4756                "    > .git",
4757                "    > dir_1  <== selected",
4758                "    > dir_2",
4759                "      .gitignore",
4760            ],
4761            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4762        );
4763    }
4764
4765    panel.update(cx, |panel, cx| {
4766        panel.project.update(cx, |_, cx| {
4767            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
4768        })
4769    });
4770    cx.run_until_parked();
4771    assert_eq!(
4772        visible_entries_as_strings(&panel, 0..20, cx),
4773        &[
4774            "v project_root",
4775            "    > .git",
4776            "    v dir_1",
4777            "        > gitignored_dir",
4778            "          file_1.py  <== selected  <== marked",
4779            "          file_2.py",
4780            "          file_3.py",
4781            "    > dir_2",
4782            "      .gitignore",
4783        ],
4784        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
4785    );
4786
4787    panel.update(cx, |panel, cx| {
4788        panel.project.update(cx, |_, cx| {
4789            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
4790        })
4791    });
4792    cx.run_until_parked();
4793    assert_eq!(
4794        visible_entries_as_strings(&panel, 0..20, cx),
4795        &[
4796            "v project_root",
4797            "    > .git",
4798            "    v dir_1",
4799            "        > gitignored_dir",
4800            "          file_1.py",
4801            "          file_2.py",
4802            "          file_3.py",
4803            "    v dir_2",
4804            "          file_1.py  <== selected  <== marked",
4805            "          file_2.py",
4806            "          file_3.py",
4807            "      .gitignore",
4808        ],
4809        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
4810    );
4811
4812    panel.update(cx, |panel, cx| {
4813        panel.project.update(cx, |_, cx| {
4814            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4815        })
4816    });
4817    cx.run_until_parked();
4818    assert_eq!(
4819        visible_entries_as_strings(&panel, 0..20, cx),
4820        &[
4821            "v project_root",
4822            "    > .git",
4823            "    v dir_1",
4824            "        v gitignored_dir",
4825            "              file_a.py  <== selected  <== marked",
4826            "              file_b.py",
4827            "              file_c.py",
4828            "          file_1.py",
4829            "          file_2.py",
4830            "          file_3.py",
4831            "    v dir_2",
4832            "          file_1.py",
4833            "          file_2.py",
4834            "          file_3.py",
4835            "      .gitignore",
4836        ],
4837        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
4838    );
4839}
4840
4841#[gpui::test]
4842async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
4843    init_test(cx);
4844    cx.update(|cx| {
4845        cx.update_global::<SettingsStore, _>(|store, cx| {
4846            store.update_user_settings(cx, |settings| {
4847                settings.project.worktree.file_scan_exclusions =
4848                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
4849            });
4850        });
4851    });
4852
4853    cx.update(|cx| {
4854        register_project_item::<TestProjectItemView>(cx);
4855    });
4856
4857    let fs = FakeFs::new(cx.executor());
4858    fs.insert_tree(
4859        "/root1",
4860        json!({
4861            ".dockerignore": "",
4862            ".git": {
4863                "HEAD": "",
4864            },
4865        }),
4866    )
4867    .await;
4868
4869    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4870    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4871    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4872    let panel = workspace
4873        .update(cx, |workspace, window, cx| {
4874            let panel = ProjectPanel::new(workspace, window, cx);
4875            workspace.add_panel(panel.clone(), window, cx);
4876            panel
4877        })
4878        .unwrap();
4879    cx.run_until_parked();
4880
4881    select_path(&panel, "root1", cx);
4882    assert_eq!(
4883        visible_entries_as_strings(&panel, 0..10, cx),
4884        &["v root1  <== selected", "      .dockerignore",]
4885    );
4886    workspace
4887        .update(cx, |workspace, _, cx| {
4888            assert!(
4889                workspace.active_item(cx).is_none(),
4890                "Should have no active items in the beginning"
4891            );
4892        })
4893        .unwrap();
4894
4895    let excluded_file_path = ".git/COMMIT_EDITMSG";
4896    let excluded_dir_path = "excluded_dir";
4897
4898    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
4899    cx.run_until_parked();
4900    panel.update_in(cx, |panel, window, cx| {
4901        assert!(panel.filename_editor.read(cx).is_focused(window));
4902    });
4903    panel
4904        .update_in(cx, |panel, window, cx| {
4905            panel.filename_editor.update(cx, |editor, cx| {
4906                editor.set_text(excluded_file_path, window, cx)
4907            });
4908            panel.confirm_edit(true, window, cx).unwrap()
4909        })
4910        .await
4911        .unwrap();
4912
4913    assert_eq!(
4914        visible_entries_as_strings(&panel, 0..13, cx),
4915        &["v root1", "      .dockerignore"],
4916        "Excluded dir should not be shown after opening a file in it"
4917    );
4918    panel.update_in(cx, |panel, window, cx| {
4919        assert!(
4920            !panel.filename_editor.read(cx).is_focused(window),
4921            "Should have closed the file name editor"
4922        );
4923    });
4924    workspace
4925        .update(cx, |workspace, _, cx| {
4926            let active_entry_path = workspace
4927                .active_item(cx)
4928                .expect("should have opened and activated the excluded item")
4929                .act_as::<TestProjectItemView>(cx)
4930                .expect("should have opened the corresponding project item for the excluded item")
4931                .read(cx)
4932                .path
4933                .clone();
4934            assert_eq!(
4935                active_entry_path.path.as_ref(),
4936                rel_path(excluded_file_path),
4937                "Should open the excluded file"
4938            );
4939
4940            assert!(
4941                workspace.notification_ids().is_empty(),
4942                "Should have no notifications after opening an excluded file"
4943            );
4944        })
4945        .unwrap();
4946    assert!(
4947        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
4948        "Should have created the excluded file"
4949    );
4950
4951    select_path(&panel, "root1", cx);
4952    panel.update_in(cx, |panel, window, cx| {
4953        panel.new_directory(&NewDirectory, window, cx)
4954    });
4955    cx.run_until_parked();
4956    panel.update_in(cx, |panel, window, cx| {
4957        assert!(panel.filename_editor.read(cx).is_focused(window));
4958    });
4959    panel
4960        .update_in(cx, |panel, window, cx| {
4961            panel.filename_editor.update(cx, |editor, cx| {
4962                editor.set_text(excluded_file_path, window, cx)
4963            });
4964            panel.confirm_edit(true, window, cx).unwrap()
4965        })
4966        .await
4967        .unwrap();
4968    cx.run_until_parked();
4969    assert_eq!(
4970        visible_entries_as_strings(&panel, 0..13, cx),
4971        &["v root1", "      .dockerignore"],
4972        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
4973    );
4974    panel.update_in(cx, |panel, window, cx| {
4975        assert!(
4976            !panel.filename_editor.read(cx).is_focused(window),
4977            "Should have closed the file name editor"
4978        );
4979    });
4980    workspace
4981        .update(cx, |workspace, _, cx| {
4982            let notifications = workspace.notification_ids();
4983            assert_eq!(
4984                notifications.len(),
4985                1,
4986                "Should receive one notification with the error message"
4987            );
4988            workspace.dismiss_notification(notifications.first().unwrap(), cx);
4989            assert!(workspace.notification_ids().is_empty());
4990        })
4991        .unwrap();
4992
4993    select_path(&panel, "root1", cx);
4994    panel.update_in(cx, |panel, window, cx| {
4995        panel.new_directory(&NewDirectory, window, cx)
4996    });
4997    cx.run_until_parked();
4998
4999    panel.update_in(cx, |panel, window, cx| {
5000        assert!(panel.filename_editor.read(cx).is_focused(window));
5001    });
5002
5003    panel
5004        .update_in(cx, |panel, window, cx| {
5005            panel.filename_editor.update(cx, |editor, cx| {
5006                editor.set_text(excluded_dir_path, window, cx)
5007            });
5008            panel.confirm_edit(true, window, cx).unwrap()
5009        })
5010        .await
5011        .unwrap();
5012
5013    cx.run_until_parked();
5014
5015    assert_eq!(
5016        visible_entries_as_strings(&panel, 0..13, cx),
5017        &["v root1", "      .dockerignore"],
5018        "Should not change the project panel after trying to create an excluded directory"
5019    );
5020    panel.update_in(cx, |panel, window, cx| {
5021        assert!(
5022            !panel.filename_editor.read(cx).is_focused(window),
5023            "Should have closed the file name editor"
5024        );
5025    });
5026    workspace
5027        .update(cx, |workspace, _, cx| {
5028            let notifications = workspace.notification_ids();
5029            assert_eq!(
5030                notifications.len(),
5031                1,
5032                "Should receive one notification explaining that no directory is actually shown"
5033            );
5034            workspace.dismiss_notification(notifications.first().unwrap(), cx);
5035            assert!(workspace.notification_ids().is_empty());
5036        })
5037        .unwrap();
5038    assert!(
5039        fs.is_dir(Path::new("/root1/excluded_dir")).await,
5040        "Should have created the excluded directory"
5041    );
5042}
5043
5044#[gpui::test]
5045async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
5046    init_test_with_editor(cx);
5047
5048    let fs = FakeFs::new(cx.executor());
5049    fs.insert_tree(
5050        "/src",
5051        json!({
5052            "test": {
5053                "first.rs": "// First Rust file",
5054                "second.rs": "// Second Rust file",
5055                "third.rs": "// Third Rust file",
5056            }
5057        }),
5058    )
5059    .await;
5060
5061    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
5062    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5063    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5064    let panel = workspace
5065        .update(cx, |workspace, window, cx| {
5066            let panel = ProjectPanel::new(workspace, window, cx);
5067            workspace.add_panel(panel.clone(), window, cx);
5068            panel
5069        })
5070        .unwrap();
5071    cx.run_until_parked();
5072
5073    select_path(&panel, "src", cx);
5074    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
5075    cx.executor().run_until_parked();
5076    assert_eq!(
5077        visible_entries_as_strings(&panel, 0..10, cx),
5078        &[
5079            //
5080            "v src  <== selected",
5081            "    > test"
5082        ]
5083    );
5084    panel.update_in(cx, |panel, window, cx| {
5085        panel.new_directory(&NewDirectory, window, cx)
5086    });
5087    cx.executor().run_until_parked();
5088    panel.update_in(cx, |panel, window, cx| {
5089        assert!(panel.filename_editor.read(cx).is_focused(window));
5090    });
5091    assert_eq!(
5092        visible_entries_as_strings(&panel, 0..10, cx),
5093        &[
5094            //
5095            "v src",
5096            "    > [EDITOR: '']  <== selected",
5097            "    > test"
5098        ]
5099    );
5100
5101    panel.update_in(cx, |panel, window, cx| {
5102        panel.cancel(&menu::Cancel, window, cx);
5103        panel.update_visible_entries(None, None, false, window, cx);
5104    });
5105    cx.executor().run_until_parked();
5106    assert_eq!(
5107        visible_entries_as_strings(&panel, 0..10, cx),
5108        &[
5109            //
5110            "v src  <== selected",
5111            "    > test"
5112        ]
5113    );
5114}
5115
5116#[gpui::test]
5117async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
5118    init_test_with_editor(cx);
5119
5120    let fs = FakeFs::new(cx.executor());
5121    fs.insert_tree(
5122        "/root",
5123        json!({
5124            "dir1": {
5125                "subdir1": {},
5126                "file1.txt": "",
5127                "file2.txt": "",
5128            },
5129            "dir2": {
5130                "subdir2": {},
5131                "file3.txt": "",
5132                "file4.txt": "",
5133            },
5134            "file5.txt": "",
5135            "file6.txt": "",
5136        }),
5137    )
5138    .await;
5139
5140    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5141    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5142    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5143    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5144    cx.run_until_parked();
5145
5146    toggle_expand_dir(&panel, "root/dir1", cx);
5147    toggle_expand_dir(&panel, "root/dir2", cx);
5148
5149    // Test Case 1: Delete middle file in directory
5150    select_path(&panel, "root/dir1/file1.txt", cx);
5151    assert_eq!(
5152        visible_entries_as_strings(&panel, 0..15, cx),
5153        &[
5154            "v root",
5155            "    v dir1",
5156            "        > subdir1",
5157            "          file1.txt  <== selected",
5158            "          file2.txt",
5159            "    v dir2",
5160            "        > subdir2",
5161            "          file3.txt",
5162            "          file4.txt",
5163            "      file5.txt",
5164            "      file6.txt",
5165        ],
5166        "Initial state before deleting middle file"
5167    );
5168
5169    submit_deletion(&panel, cx);
5170    assert_eq!(
5171        visible_entries_as_strings(&panel, 0..15, cx),
5172        &[
5173            "v root",
5174            "    v dir1",
5175            "        > subdir1",
5176            "          file2.txt  <== selected",
5177            "    v dir2",
5178            "        > subdir2",
5179            "          file3.txt",
5180            "          file4.txt",
5181            "      file5.txt",
5182            "      file6.txt",
5183        ],
5184        "Should select next file after deleting middle file"
5185    );
5186
5187    // Test Case 2: Delete last file in directory
5188    submit_deletion(&panel, cx);
5189    assert_eq!(
5190        visible_entries_as_strings(&panel, 0..15, cx),
5191        &[
5192            "v root",
5193            "    v dir1",
5194            "        > subdir1  <== selected",
5195            "    v dir2",
5196            "        > subdir2",
5197            "          file3.txt",
5198            "          file4.txt",
5199            "      file5.txt",
5200            "      file6.txt",
5201        ],
5202        "Should select next directory when last file is deleted"
5203    );
5204
5205    // Test Case 3: Delete root level file
5206    select_path(&panel, "root/file6.txt", cx);
5207    assert_eq!(
5208        visible_entries_as_strings(&panel, 0..15, cx),
5209        &[
5210            "v root",
5211            "    v dir1",
5212            "        > subdir1",
5213            "    v dir2",
5214            "        > subdir2",
5215            "          file3.txt",
5216            "          file4.txt",
5217            "      file5.txt",
5218            "      file6.txt  <== selected",
5219        ],
5220        "Initial state before deleting root level file"
5221    );
5222
5223    submit_deletion(&panel, cx);
5224    assert_eq!(
5225        visible_entries_as_strings(&panel, 0..15, cx),
5226        &[
5227            "v root",
5228            "    v dir1",
5229            "        > subdir1",
5230            "    v dir2",
5231            "        > subdir2",
5232            "          file3.txt",
5233            "          file4.txt",
5234            "      file5.txt  <== selected",
5235        ],
5236        "Should select prev entry at root level"
5237    );
5238}
5239
5240#[gpui::test]
5241async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
5242    init_test_with_editor(cx);
5243
5244    let fs = FakeFs::new(cx.executor());
5245    fs.insert_tree(
5246        path!("/root"),
5247        json!({
5248            "aa": "// Testing 1",
5249            "bb": "// Testing 2",
5250            "cc": "// Testing 3",
5251            "dd": "// Testing 4",
5252            "ee": "// Testing 5",
5253            "ff": "// Testing 6",
5254            "gg": "// Testing 7",
5255            "hh": "// Testing 8",
5256            "ii": "// Testing 8",
5257            ".gitignore": "bb\ndd\nee\nff\nii\n'",
5258        }),
5259    )
5260    .await;
5261
5262    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5263    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5264    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5265
5266    // Test 1: Auto selection with one gitignored file next to the deleted file
5267    cx.update(|_, cx| {
5268        let settings = *ProjectPanelSettings::get_global(cx);
5269        ProjectPanelSettings::override_global(
5270            ProjectPanelSettings {
5271                hide_gitignore: true,
5272                ..settings
5273            },
5274            cx,
5275        );
5276    });
5277
5278    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5279    cx.run_until_parked();
5280
5281    select_path(&panel, "root/aa", cx);
5282    assert_eq!(
5283        visible_entries_as_strings(&panel, 0..10, cx),
5284        &[
5285            "v root",
5286            "      .gitignore",
5287            "      aa  <== selected",
5288            "      cc",
5289            "      gg",
5290            "      hh"
5291        ],
5292        "Initial state should hide files on .gitignore"
5293    );
5294
5295    submit_deletion(&panel, cx);
5296
5297    assert_eq!(
5298        visible_entries_as_strings(&panel, 0..10, cx),
5299        &[
5300            "v root",
5301            "      .gitignore",
5302            "      cc  <== selected",
5303            "      gg",
5304            "      hh"
5305        ],
5306        "Should select next entry not on .gitignore"
5307    );
5308
5309    // Test 2: Auto selection with many gitignored files next to the deleted file
5310    submit_deletion(&panel, cx);
5311    assert_eq!(
5312        visible_entries_as_strings(&panel, 0..10, cx),
5313        &[
5314            "v root",
5315            "      .gitignore",
5316            "      gg  <== selected",
5317            "      hh"
5318        ],
5319        "Should select next entry not on .gitignore"
5320    );
5321
5322    // Test 3: Auto selection of entry before deleted file
5323    select_path(&panel, "root/hh", cx);
5324    assert_eq!(
5325        visible_entries_as_strings(&panel, 0..10, cx),
5326        &[
5327            "v root",
5328            "      .gitignore",
5329            "      gg",
5330            "      hh  <== selected"
5331        ],
5332        "Should select next entry not on .gitignore"
5333    );
5334    submit_deletion(&panel, cx);
5335    assert_eq!(
5336        visible_entries_as_strings(&panel, 0..10, cx),
5337        &["v root", "      .gitignore", "      gg  <== selected"],
5338        "Should select next entry not on .gitignore"
5339    );
5340}
5341
5342#[gpui::test]
5343async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
5344    init_test_with_editor(cx);
5345
5346    let fs = FakeFs::new(cx.executor());
5347    fs.insert_tree(
5348        path!("/root"),
5349        json!({
5350            "dir1": {
5351                "file1": "// Testing",
5352                "file2": "// Testing",
5353                "file3": "// Testing"
5354            },
5355            "aa": "// Testing",
5356            ".gitignore": "file1\nfile3\n",
5357        }),
5358    )
5359    .await;
5360
5361    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5362    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5363    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5364
5365    cx.update(|_, cx| {
5366        let settings = *ProjectPanelSettings::get_global(cx);
5367        ProjectPanelSettings::override_global(
5368            ProjectPanelSettings {
5369                hide_gitignore: true,
5370                ..settings
5371            },
5372            cx,
5373        );
5374    });
5375
5376    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5377    cx.run_until_parked();
5378
5379    // Test 1: Visible items should exclude files on gitignore
5380    toggle_expand_dir(&panel, "root/dir1", cx);
5381    select_path(&panel, "root/dir1/file2", cx);
5382    assert_eq!(
5383        visible_entries_as_strings(&panel, 0..10, cx),
5384        &[
5385            "v root",
5386            "    v dir1",
5387            "          file2  <== selected",
5388            "      .gitignore",
5389            "      aa"
5390        ],
5391        "Initial state should hide files on .gitignore"
5392    );
5393    submit_deletion(&panel, cx);
5394
5395    // Test 2: Auto selection should go to the parent
5396    assert_eq!(
5397        visible_entries_as_strings(&panel, 0..10, cx),
5398        &[
5399            "v root",
5400            "    v dir1  <== selected",
5401            "      .gitignore",
5402            "      aa"
5403        ],
5404        "Initial state should hide files on .gitignore"
5405    );
5406}
5407
5408#[gpui::test]
5409async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
5410    init_test_with_editor(cx);
5411
5412    let fs = FakeFs::new(cx.executor());
5413    fs.insert_tree(
5414        "/root",
5415        json!({
5416            "dir1": {
5417                "subdir1": {
5418                    "a.txt": "",
5419                    "b.txt": ""
5420                },
5421                "file1.txt": "",
5422            },
5423            "dir2": {
5424                "subdir2": {
5425                    "c.txt": "",
5426                    "d.txt": ""
5427                },
5428                "file2.txt": "",
5429            },
5430            "file3.txt": "",
5431        }),
5432    )
5433    .await;
5434
5435    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5436    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5437    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5438    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5439    cx.run_until_parked();
5440
5441    toggle_expand_dir(&panel, "root/dir1", cx);
5442    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5443    toggle_expand_dir(&panel, "root/dir2", cx);
5444    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5445
5446    // Test Case 1: Select and delete nested directory with parent
5447    cx.simulate_modifiers_change(gpui::Modifiers {
5448        control: true,
5449        ..Default::default()
5450    });
5451    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5452    select_path_with_mark(&panel, "root/dir1", cx);
5453
5454    assert_eq!(
5455        visible_entries_as_strings(&panel, 0..15, cx),
5456        &[
5457            "v root",
5458            "    v dir1  <== selected  <== marked",
5459            "        v subdir1  <== marked",
5460            "              a.txt",
5461            "              b.txt",
5462            "          file1.txt",
5463            "    v dir2",
5464            "        v subdir2",
5465            "              c.txt",
5466            "              d.txt",
5467            "          file2.txt",
5468            "      file3.txt",
5469        ],
5470        "Initial state before deleting nested directory with parent"
5471    );
5472
5473    submit_deletion(&panel, cx);
5474    assert_eq!(
5475        visible_entries_as_strings(&panel, 0..15, cx),
5476        &[
5477            "v root",
5478            "    v dir2  <== selected",
5479            "        v subdir2",
5480            "              c.txt",
5481            "              d.txt",
5482            "          file2.txt",
5483            "      file3.txt",
5484        ],
5485        "Should select next directory after deleting directory with parent"
5486    );
5487
5488    // Test Case 2: Select mixed files and directories across levels
5489    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
5490    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
5491    select_path_with_mark(&panel, "root/file3.txt", cx);
5492
5493    assert_eq!(
5494        visible_entries_as_strings(&panel, 0..15, cx),
5495        &[
5496            "v root",
5497            "    v dir2",
5498            "        v subdir2",
5499            "              c.txt  <== marked",
5500            "              d.txt",
5501            "          file2.txt  <== marked",
5502            "      file3.txt  <== selected  <== marked",
5503        ],
5504        "Initial state before deleting"
5505    );
5506
5507    submit_deletion(&panel, cx);
5508    assert_eq!(
5509        visible_entries_as_strings(&panel, 0..15, cx),
5510        &[
5511            "v root",
5512            "    v dir2  <== selected",
5513            "        v subdir2",
5514            "              d.txt",
5515        ],
5516        "Should select sibling directory"
5517    );
5518}
5519
5520#[gpui::test]
5521async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
5522    init_test_with_editor(cx);
5523
5524    let fs = FakeFs::new(cx.executor());
5525    fs.insert_tree(
5526        "/root",
5527        json!({
5528            "dir1": {
5529                "subdir1": {
5530                    "a.txt": "",
5531                    "b.txt": ""
5532                },
5533                "file1.txt": "",
5534            },
5535            "dir2": {
5536                "subdir2": {
5537                    "c.txt": "",
5538                    "d.txt": ""
5539                },
5540                "file2.txt": "",
5541            },
5542            "file3.txt": "",
5543            "file4.txt": "",
5544        }),
5545    )
5546    .await;
5547
5548    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5549    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5550    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5551    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5552    cx.run_until_parked();
5553
5554    toggle_expand_dir(&panel, "root/dir1", cx);
5555    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5556    toggle_expand_dir(&panel, "root/dir2", cx);
5557    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5558
5559    // Test Case 1: Select all root files and directories
5560    cx.simulate_modifiers_change(gpui::Modifiers {
5561        control: true,
5562        ..Default::default()
5563    });
5564    select_path_with_mark(&panel, "root/dir1", cx);
5565    select_path_with_mark(&panel, "root/dir2", cx);
5566    select_path_with_mark(&panel, "root/file3.txt", cx);
5567    select_path_with_mark(&panel, "root/file4.txt", cx);
5568    assert_eq!(
5569        visible_entries_as_strings(&panel, 0..20, cx),
5570        &[
5571            "v root",
5572            "    v dir1  <== marked",
5573            "        v subdir1",
5574            "              a.txt",
5575            "              b.txt",
5576            "          file1.txt",
5577            "    v dir2  <== marked",
5578            "        v subdir2",
5579            "              c.txt",
5580            "              d.txt",
5581            "          file2.txt",
5582            "      file3.txt  <== marked",
5583            "      file4.txt  <== selected  <== marked",
5584        ],
5585        "State before deleting all contents"
5586    );
5587
5588    submit_deletion(&panel, cx);
5589    assert_eq!(
5590        visible_entries_as_strings(&panel, 0..20, cx),
5591        &["v root  <== selected"],
5592        "Only empty root directory should remain after deleting all contents"
5593    );
5594}
5595
5596#[gpui::test]
5597async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
5598    init_test_with_editor(cx);
5599
5600    let fs = FakeFs::new(cx.executor());
5601    fs.insert_tree(
5602        "/root",
5603        json!({
5604            "dir1": {
5605                "subdir1": {
5606                    "file_a.txt": "content a",
5607                    "file_b.txt": "content b",
5608                },
5609                "subdir2": {
5610                    "file_c.txt": "content c",
5611                },
5612                "file1.txt": "content 1",
5613            },
5614            "dir2": {
5615                "file2.txt": "content 2",
5616            },
5617        }),
5618    )
5619    .await;
5620
5621    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5622    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5623    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5624    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5625    cx.run_until_parked();
5626
5627    toggle_expand_dir(&panel, "root/dir1", cx);
5628    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5629    toggle_expand_dir(&panel, "root/dir2", cx);
5630    cx.simulate_modifiers_change(gpui::Modifiers {
5631        control: true,
5632        ..Default::default()
5633    });
5634
5635    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
5636    select_path_with_mark(&panel, "root/dir1", cx);
5637    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5638    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
5639
5640    assert_eq!(
5641        visible_entries_as_strings(&panel, 0..20, cx),
5642        &[
5643            "v root",
5644            "    v dir1  <== marked",
5645            "        v subdir1  <== marked",
5646            "              file_a.txt  <== selected  <== marked",
5647            "              file_b.txt",
5648            "        > subdir2",
5649            "          file1.txt",
5650            "    v dir2",
5651            "          file2.txt",
5652        ],
5653        "State with parent dir, subdir, and file selected"
5654    );
5655    submit_deletion(&panel, cx);
5656    assert_eq!(
5657        visible_entries_as_strings(&panel, 0..20, cx),
5658        &["v root", "    v dir2  <== selected", "          file2.txt",],
5659        "Only dir2 should remain after deletion"
5660    );
5661}
5662
5663#[gpui::test]
5664async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
5665    init_test_with_editor(cx);
5666
5667    let fs = FakeFs::new(cx.executor());
5668    // First worktree
5669    fs.insert_tree(
5670        "/root1",
5671        json!({
5672            "dir1": {
5673                "file1.txt": "content 1",
5674                "file2.txt": "content 2",
5675            },
5676            "dir2": {
5677                "file3.txt": "content 3",
5678            },
5679        }),
5680    )
5681    .await;
5682
5683    // Second worktree
5684    fs.insert_tree(
5685        "/root2",
5686        json!({
5687            "dir3": {
5688                "file4.txt": "content 4",
5689                "file5.txt": "content 5",
5690            },
5691            "file6.txt": "content 6",
5692        }),
5693    )
5694    .await;
5695
5696    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5697    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5698    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5699    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5700    cx.run_until_parked();
5701
5702    // Expand all directories for testing
5703    toggle_expand_dir(&panel, "root1/dir1", cx);
5704    toggle_expand_dir(&panel, "root1/dir2", cx);
5705    toggle_expand_dir(&panel, "root2/dir3", cx);
5706
5707    // Test Case 1: Delete files across different worktrees
5708    cx.simulate_modifiers_change(gpui::Modifiers {
5709        control: true,
5710        ..Default::default()
5711    });
5712    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
5713    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
5714
5715    assert_eq!(
5716        visible_entries_as_strings(&panel, 0..20, cx),
5717        &[
5718            "v root1",
5719            "    v dir1",
5720            "          file1.txt  <== marked",
5721            "          file2.txt",
5722            "    v dir2",
5723            "          file3.txt",
5724            "v root2",
5725            "    v dir3",
5726            "          file4.txt  <== selected  <== marked",
5727            "          file5.txt",
5728            "      file6.txt",
5729        ],
5730        "Initial state with files selected from different worktrees"
5731    );
5732
5733    submit_deletion(&panel, cx);
5734    assert_eq!(
5735        visible_entries_as_strings(&panel, 0..20, cx),
5736        &[
5737            "v root1",
5738            "    v dir1",
5739            "          file2.txt",
5740            "    v dir2",
5741            "          file3.txt",
5742            "v root2",
5743            "    v dir3",
5744            "          file5.txt  <== selected",
5745            "      file6.txt",
5746        ],
5747        "Should select next file in the last worktree after deletion"
5748    );
5749
5750    // Test Case 2: Delete directories from different worktrees
5751    select_path_with_mark(&panel, "root1/dir1", cx);
5752    select_path_with_mark(&panel, "root2/dir3", cx);
5753
5754    assert_eq!(
5755        visible_entries_as_strings(&panel, 0..20, cx),
5756        &[
5757            "v root1",
5758            "    v dir1  <== marked",
5759            "          file2.txt",
5760            "    v dir2",
5761            "          file3.txt",
5762            "v root2",
5763            "    v dir3  <== selected  <== marked",
5764            "          file5.txt",
5765            "      file6.txt",
5766        ],
5767        "State with directories marked from different worktrees"
5768    );
5769
5770    submit_deletion(&panel, cx);
5771    assert_eq!(
5772        visible_entries_as_strings(&panel, 0..20, cx),
5773        &[
5774            "v root1",
5775            "    v dir2",
5776            "          file3.txt",
5777            "v root2",
5778            "      file6.txt  <== selected",
5779        ],
5780        "Should select remaining file in last worktree after directory deletion"
5781    );
5782
5783    // Test Case 4: Delete all remaining files except roots
5784    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
5785    select_path_with_mark(&panel, "root2/file6.txt", cx);
5786
5787    assert_eq!(
5788        visible_entries_as_strings(&panel, 0..20, cx),
5789        &[
5790            "v root1",
5791            "    v dir2",
5792            "          file3.txt  <== marked",
5793            "v root2",
5794            "      file6.txt  <== selected  <== marked",
5795        ],
5796        "State with all remaining files marked"
5797    );
5798
5799    submit_deletion(&panel, cx);
5800    assert_eq!(
5801        visible_entries_as_strings(&panel, 0..20, cx),
5802        &["v root1", "    v dir2", "v root2  <== selected"],
5803        "Second parent root should be selected after deleting"
5804    );
5805}
5806
5807#[gpui::test]
5808async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
5809    init_test_with_editor(cx);
5810
5811    let fs = FakeFs::new(cx.executor());
5812    fs.insert_tree(
5813        "/root",
5814        json!({
5815            "dir1": {
5816                "file1.txt": "",
5817                "file2.txt": "",
5818                "file3.txt": "",
5819            },
5820            "dir2": {
5821                "file4.txt": "",
5822                "file5.txt": "",
5823            },
5824        }),
5825    )
5826    .await;
5827
5828    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5829    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5830    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5831    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5832    cx.run_until_parked();
5833
5834    toggle_expand_dir(&panel, "root/dir1", cx);
5835    toggle_expand_dir(&panel, "root/dir2", cx);
5836
5837    cx.simulate_modifiers_change(gpui::Modifiers {
5838        control: true,
5839        ..Default::default()
5840    });
5841
5842    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
5843    select_path(&panel, "root/dir1/file1.txt", cx);
5844
5845    assert_eq!(
5846        visible_entries_as_strings(&panel, 0..15, cx),
5847        &[
5848            "v root",
5849            "    v dir1",
5850            "          file1.txt  <== selected",
5851            "          file2.txt  <== marked",
5852            "          file3.txt",
5853            "    v dir2",
5854            "          file4.txt",
5855            "          file5.txt",
5856        ],
5857        "Initial state with one marked entry and different selection"
5858    );
5859
5860    // Delete should operate on the selected entry (file1.txt)
5861    submit_deletion(&panel, cx);
5862    assert_eq!(
5863        visible_entries_as_strings(&panel, 0..15, cx),
5864        &[
5865            "v root",
5866            "    v dir1",
5867            "          file2.txt  <== selected  <== marked",
5868            "          file3.txt",
5869            "    v dir2",
5870            "          file4.txt",
5871            "          file5.txt",
5872        ],
5873        "Should delete selected file, not marked file"
5874    );
5875
5876    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
5877    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
5878    select_path(&panel, "root/dir2/file5.txt", cx);
5879
5880    assert_eq!(
5881        visible_entries_as_strings(&panel, 0..15, cx),
5882        &[
5883            "v root",
5884            "    v dir1",
5885            "          file2.txt  <== marked",
5886            "          file3.txt  <== marked",
5887            "    v dir2",
5888            "          file4.txt  <== marked",
5889            "          file5.txt  <== selected",
5890        ],
5891        "Initial state with multiple marked entries and different selection"
5892    );
5893
5894    // Delete should operate on all marked entries, ignoring the selection
5895    submit_deletion(&panel, cx);
5896    assert_eq!(
5897        visible_entries_as_strings(&panel, 0..15, cx),
5898        &[
5899            "v root",
5900            "    v dir1",
5901            "    v dir2",
5902            "          file5.txt  <== selected",
5903        ],
5904        "Should delete all marked files, leaving only the selected file"
5905    );
5906}
5907
5908#[gpui::test]
5909async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
5910    init_test_with_editor(cx);
5911
5912    let fs = FakeFs::new(cx.executor());
5913    fs.insert_tree(
5914        "/root_b",
5915        json!({
5916            "dir1": {
5917                "file1.txt": "content 1",
5918                "file2.txt": "content 2",
5919            },
5920        }),
5921    )
5922    .await;
5923
5924    fs.insert_tree(
5925        "/root_c",
5926        json!({
5927            "dir2": {},
5928        }),
5929    )
5930    .await;
5931
5932    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
5933    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5934    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5935    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5936    cx.run_until_parked();
5937
5938    toggle_expand_dir(&panel, "root_b/dir1", cx);
5939    toggle_expand_dir(&panel, "root_c/dir2", cx);
5940
5941    cx.simulate_modifiers_change(gpui::Modifiers {
5942        control: true,
5943        ..Default::default()
5944    });
5945    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
5946    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
5947
5948    assert_eq!(
5949        visible_entries_as_strings(&panel, 0..20, cx),
5950        &[
5951            "v root_b",
5952            "    v dir1",
5953            "          file1.txt  <== marked",
5954            "          file2.txt  <== selected  <== marked",
5955            "v root_c",
5956            "    v dir2",
5957        ],
5958        "Initial state with files marked in root_b"
5959    );
5960
5961    submit_deletion(&panel, cx);
5962    assert_eq!(
5963        visible_entries_as_strings(&panel, 0..20, cx),
5964        &[
5965            "v root_b",
5966            "    v dir1  <== selected",
5967            "v root_c",
5968            "    v dir2",
5969        ],
5970        "After deletion in root_b as it's last deletion, selection should be in root_b"
5971    );
5972
5973    select_path_with_mark(&panel, "root_c/dir2", cx);
5974
5975    submit_deletion(&panel, cx);
5976    assert_eq!(
5977        visible_entries_as_strings(&panel, 0..20, cx),
5978        &["v root_b", "    v dir1", "v root_c  <== selected",],
5979        "After deleting from root_c, it should remain in root_c"
5980    );
5981}
5982
5983fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
5984    let path = rel_path(path);
5985    panel.update_in(cx, |panel, window, cx| {
5986        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5987            let worktree = worktree.read(cx);
5988            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5989                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5990                panel.toggle_expanded(entry_id, window, cx);
5991                return;
5992            }
5993        }
5994        panic!("no worktree for path {:?}", path);
5995    });
5996    cx.run_until_parked();
5997}
5998
5999#[gpui::test]
6000async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
6001    init_test_with_editor(cx);
6002
6003    let fs = FakeFs::new(cx.executor());
6004    fs.insert_tree(
6005        path!("/root"),
6006        json!({
6007            ".gitignore": "**/ignored_dir\n**/ignored_nested",
6008            "dir1": {
6009                "empty1": {
6010                    "empty2": {
6011                        "empty3": {
6012                            "file.txt": ""
6013                        }
6014                    }
6015                },
6016                "subdir1": {
6017                    "file1.txt": "",
6018                    "file2.txt": "",
6019                    "ignored_nested": {
6020                        "ignored_file.txt": ""
6021                    }
6022                },
6023                "ignored_dir": {
6024                    "subdir": {
6025                        "deep_file.txt": ""
6026                    }
6027                }
6028            }
6029        }),
6030    )
6031    .await;
6032
6033    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6034    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6035    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6036
6037    // Test 1: When auto-fold is enabled
6038    cx.update(|_, cx| {
6039        let settings = *ProjectPanelSettings::get_global(cx);
6040        ProjectPanelSettings::override_global(
6041            ProjectPanelSettings {
6042                auto_fold_dirs: true,
6043                ..settings
6044            },
6045            cx,
6046        );
6047    });
6048
6049    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6050    cx.run_until_parked();
6051
6052    assert_eq!(
6053        visible_entries_as_strings(&panel, 0..20, cx),
6054        &["v root", "    > dir1", "      .gitignore",],
6055        "Initial state should show collapsed root structure"
6056    );
6057
6058    toggle_expand_dir(&panel, "root/dir1", cx);
6059    assert_eq!(
6060        visible_entries_as_strings(&panel, 0..20, cx),
6061        &[
6062            "v root",
6063            "    v dir1  <== selected",
6064            "        > empty1/empty2/empty3",
6065            "        > ignored_dir",
6066            "        > subdir1",
6067            "      .gitignore",
6068        ],
6069        "Should show first level with auto-folded dirs and ignored dir visible"
6070    );
6071
6072    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6073    panel.update_in(cx, |panel, window, cx| {
6074        let project = panel.project.read(cx);
6075        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6076        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6077        panel.update_visible_entries(None, None, false, window, cx);
6078    });
6079    cx.run_until_parked();
6080
6081    assert_eq!(
6082        visible_entries_as_strings(&panel, 0..20, cx),
6083        &[
6084            "v root",
6085            "    v dir1  <== selected",
6086            "        v empty1",
6087            "            v empty2",
6088            "                v empty3",
6089            "                      file.txt",
6090            "        > ignored_dir",
6091            "        v subdir1",
6092            "            > ignored_nested",
6093            "              file1.txt",
6094            "              file2.txt",
6095            "      .gitignore",
6096        ],
6097        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
6098    );
6099
6100    // Test 2: When auto-fold is disabled
6101    cx.update(|_, cx| {
6102        let settings = *ProjectPanelSettings::get_global(cx);
6103        ProjectPanelSettings::override_global(
6104            ProjectPanelSettings {
6105                auto_fold_dirs: false,
6106                ..settings
6107            },
6108            cx,
6109        );
6110    });
6111
6112    panel.update_in(cx, |panel, window, cx| {
6113        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6114    });
6115
6116    toggle_expand_dir(&panel, "root/dir1", cx);
6117    assert_eq!(
6118        visible_entries_as_strings(&panel, 0..20, cx),
6119        &[
6120            "v root",
6121            "    v dir1  <== selected",
6122            "        > empty1",
6123            "        > ignored_dir",
6124            "        > subdir1",
6125            "      .gitignore",
6126        ],
6127        "With auto-fold disabled: should show all directories separately"
6128    );
6129
6130    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6131    panel.update_in(cx, |panel, window, cx| {
6132        let project = panel.project.read(cx);
6133        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6134        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6135        panel.update_visible_entries(None, None, false, window, cx);
6136    });
6137    cx.run_until_parked();
6138
6139    assert_eq!(
6140        visible_entries_as_strings(&panel, 0..20, cx),
6141        &[
6142            "v root",
6143            "    v dir1  <== selected",
6144            "        v empty1",
6145            "            v empty2",
6146            "                v empty3",
6147            "                      file.txt",
6148            "        > ignored_dir",
6149            "        v subdir1",
6150            "            > ignored_nested",
6151            "              file1.txt",
6152            "              file2.txt",
6153            "      .gitignore",
6154        ],
6155        "After expand_all without auto-fold: should expand all dirs normally, \
6156         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
6157    );
6158
6159    // Test 3: When explicitly called on ignored directory
6160    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
6161    panel.update_in(cx, |panel, window, cx| {
6162        let project = panel.project.read(cx);
6163        let worktree = project.worktrees(cx).next().unwrap().read(cx);
6164        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
6165        panel.update_visible_entries(None, None, false, window, cx);
6166    });
6167    cx.run_until_parked();
6168
6169    assert_eq!(
6170        visible_entries_as_strings(&panel, 0..20, cx),
6171        &[
6172            "v root",
6173            "    v dir1  <== selected",
6174            "        v empty1",
6175            "            v empty2",
6176            "                v empty3",
6177            "                      file.txt",
6178            "        v ignored_dir",
6179            "            v subdir",
6180            "                  deep_file.txt",
6181            "        v subdir1",
6182            "            > ignored_nested",
6183            "              file1.txt",
6184            "              file2.txt",
6185            "      .gitignore",
6186        ],
6187        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
6188    );
6189}
6190
6191#[gpui::test]
6192async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
6193    init_test(cx);
6194
6195    let fs = FakeFs::new(cx.executor());
6196    fs.insert_tree(
6197        path!("/root"),
6198        json!({
6199            "dir1": {
6200                "subdir1": {
6201                    "nested1": {
6202                        "file1.txt": "",
6203                        "file2.txt": ""
6204                    },
6205                },
6206                "subdir2": {
6207                    "file4.txt": ""
6208                }
6209            },
6210            "dir2": {
6211                "single_file": {
6212                    "file5.txt": ""
6213                }
6214            }
6215        }),
6216    )
6217    .await;
6218
6219    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6220    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6221    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6222
6223    // Test 1: Basic collapsing
6224    {
6225        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6226        cx.run_until_parked();
6227
6228        toggle_expand_dir(&panel, "root/dir1", cx);
6229        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6230        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6231        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
6232
6233        assert_eq!(
6234            visible_entries_as_strings(&panel, 0..20, cx),
6235            &[
6236                "v root",
6237                "    v dir1",
6238                "        v subdir1",
6239                "            v nested1",
6240                "                  file1.txt",
6241                "                  file2.txt",
6242                "        v subdir2  <== selected",
6243                "              file4.txt",
6244                "    > dir2",
6245            ],
6246            "Initial state with everything expanded"
6247        );
6248
6249        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6250        panel.update_in(cx, |panel, window, cx| {
6251            let project = panel.project.read(cx);
6252            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6253            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6254            panel.update_visible_entries(None, None, false, window, cx);
6255        });
6256        cx.run_until_parked();
6257
6258        assert_eq!(
6259            visible_entries_as_strings(&panel, 0..20, cx),
6260            &["v root", "    > dir1", "    > dir2",],
6261            "All subdirs under dir1 should be collapsed"
6262        );
6263    }
6264
6265    // Test 2: With auto-fold enabled
6266    {
6267        cx.update(|_, cx| {
6268            let settings = *ProjectPanelSettings::get_global(cx);
6269            ProjectPanelSettings::override_global(
6270                ProjectPanelSettings {
6271                    auto_fold_dirs: true,
6272                    ..settings
6273                },
6274                cx,
6275            );
6276        });
6277
6278        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6279        cx.run_until_parked();
6280
6281        toggle_expand_dir(&panel, "root/dir1", cx);
6282        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6283        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6284
6285        assert_eq!(
6286            visible_entries_as_strings(&panel, 0..20, cx),
6287            &[
6288                "v root",
6289                "    v dir1",
6290                "        v subdir1/nested1  <== selected",
6291                "              file1.txt",
6292                "              file2.txt",
6293                "        > subdir2",
6294                "    > dir2/single_file",
6295            ],
6296            "Initial state with some dirs expanded"
6297        );
6298
6299        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6300        panel.update(cx, |panel, cx| {
6301            let project = panel.project.read(cx);
6302            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6303            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6304        });
6305
6306        toggle_expand_dir(&panel, "root/dir1", cx);
6307
6308        assert_eq!(
6309            visible_entries_as_strings(&panel, 0..20, cx),
6310            &[
6311                "v root",
6312                "    v dir1  <== selected",
6313                "        > subdir1/nested1",
6314                "        > subdir2",
6315                "    > dir2/single_file",
6316            ],
6317            "Subdirs should be collapsed and folded with auto-fold enabled"
6318        );
6319    }
6320
6321    // Test 3: With auto-fold disabled
6322    {
6323        cx.update(|_, cx| {
6324            let settings = *ProjectPanelSettings::get_global(cx);
6325            ProjectPanelSettings::override_global(
6326                ProjectPanelSettings {
6327                    auto_fold_dirs: false,
6328                    ..settings
6329                },
6330                cx,
6331            );
6332        });
6333
6334        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6335        cx.run_until_parked();
6336
6337        toggle_expand_dir(&panel, "root/dir1", cx);
6338        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6339        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6340
6341        assert_eq!(
6342            visible_entries_as_strings(&panel, 0..20, cx),
6343            &[
6344                "v root",
6345                "    v dir1",
6346                "        v subdir1",
6347                "            v nested1  <== selected",
6348                "                  file1.txt",
6349                "                  file2.txt",
6350                "        > subdir2",
6351                "    > dir2",
6352            ],
6353            "Initial state with some dirs expanded and auto-fold disabled"
6354        );
6355
6356        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6357        panel.update(cx, |panel, cx| {
6358            let project = panel.project.read(cx);
6359            let worktree = project.worktrees(cx).next().unwrap().read(cx);
6360            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6361        });
6362
6363        toggle_expand_dir(&panel, "root/dir1", cx);
6364
6365        assert_eq!(
6366            visible_entries_as_strings(&panel, 0..20, cx),
6367            &[
6368                "v root",
6369                "    v dir1  <== selected",
6370                "        > subdir1",
6371                "        > subdir2",
6372                "    > dir2",
6373            ],
6374            "Subdirs should be collapsed but not folded with auto-fold disabled"
6375        );
6376    }
6377}
6378
6379#[gpui::test]
6380async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
6381    init_test(cx);
6382
6383    let fs = FakeFs::new(cx.executor());
6384    fs.insert_tree(
6385        path!("/root"),
6386        json!({
6387            "dir1": {
6388                "file1.txt": "",
6389            },
6390        }),
6391    )
6392    .await;
6393
6394    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6395    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6396    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6397
6398    let panel = workspace
6399        .update(cx, |workspace, window, cx| {
6400            let panel = ProjectPanel::new(workspace, window, cx);
6401            workspace.add_panel(panel.clone(), window, cx);
6402            panel
6403        })
6404        .unwrap();
6405    cx.run_until_parked();
6406
6407    #[rustfmt::skip]
6408    assert_eq!(
6409        visible_entries_as_strings(&panel, 0..20, cx),
6410        &[
6411            "v root",
6412            "    > dir1",
6413        ],
6414        "Initial state with nothing selected"
6415    );
6416
6417    panel.update_in(cx, |panel, window, cx| {
6418        panel.new_file(&NewFile, window, cx);
6419    });
6420    cx.run_until_parked();
6421    panel.update_in(cx, |panel, window, cx| {
6422        assert!(panel.filename_editor.read(cx).is_focused(window));
6423    });
6424    panel
6425        .update_in(cx, |panel, window, cx| {
6426            panel.filename_editor.update(cx, |editor, cx| {
6427                editor.set_text("hello_from_no_selections", window, cx)
6428            });
6429            panel.confirm_edit(true, window, cx).unwrap()
6430        })
6431        .await
6432        .unwrap();
6433    cx.run_until_parked();
6434    #[rustfmt::skip]
6435    assert_eq!(
6436        visible_entries_as_strings(&panel, 0..20, cx),
6437        &[
6438            "v root",
6439            "    > dir1",
6440            "      hello_from_no_selections  <== selected  <== marked",
6441        ],
6442        "A new file is created under the root directory"
6443    );
6444}
6445
6446#[gpui::test]
6447async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
6448    init_test(cx);
6449
6450    let fs = FakeFs::new(cx.executor());
6451    fs.insert_tree(
6452        path!("/root"),
6453        json!({
6454            "existing_dir": {
6455                "existing_file.txt": "",
6456            },
6457            "existing_file.txt": "",
6458        }),
6459    )
6460    .await;
6461
6462    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6463    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6464    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6465
6466    cx.update(|_, cx| {
6467        let settings = *ProjectPanelSettings::get_global(cx);
6468        ProjectPanelSettings::override_global(
6469            ProjectPanelSettings {
6470                hide_root: true,
6471                ..settings
6472            },
6473            cx,
6474        );
6475    });
6476
6477    let panel = workspace
6478        .update(cx, |workspace, window, cx| {
6479            let panel = ProjectPanel::new(workspace, window, cx);
6480            workspace.add_panel(panel.clone(), window, cx);
6481            panel
6482        })
6483        .unwrap();
6484    cx.run_until_parked();
6485
6486    #[rustfmt::skip]
6487    assert_eq!(
6488        visible_entries_as_strings(&panel, 0..20, cx),
6489        &[
6490            "> existing_dir",
6491            "  existing_file.txt",
6492        ],
6493        "Initial state with hide_root=true, root should be hidden and nothing selected"
6494    );
6495
6496    panel.update(cx, |panel, _| {
6497        assert!(
6498            panel.state.selection.is_none(),
6499            "Should have no selection initially"
6500        );
6501    });
6502
6503    // Test 1: Create new file when no entry is selected
6504    panel.update_in(cx, |panel, window, cx| {
6505        panel.new_file(&NewFile, window, cx);
6506    });
6507    cx.run_until_parked();
6508    panel.update_in(cx, |panel, window, cx| {
6509        assert!(panel.filename_editor.read(cx).is_focused(window));
6510    });
6511    cx.run_until_parked();
6512    #[rustfmt::skip]
6513    assert_eq!(
6514        visible_entries_as_strings(&panel, 0..20, cx),
6515        &[
6516            "> existing_dir",
6517            "  [EDITOR: '']  <== selected",
6518            "  existing_file.txt",
6519        ],
6520        "Editor should appear at root level when hide_root=true and no selection"
6521    );
6522
6523    let confirm = panel.update_in(cx, |panel, window, cx| {
6524        panel.filename_editor.update(cx, |editor, cx| {
6525            editor.set_text("new_file_at_root.txt", window, cx)
6526        });
6527        panel.confirm_edit(true, window, cx).unwrap()
6528    });
6529    confirm.await.unwrap();
6530    cx.run_until_parked();
6531
6532    #[rustfmt::skip]
6533    assert_eq!(
6534        visible_entries_as_strings(&panel, 0..20, cx),
6535        &[
6536            "> existing_dir",
6537            "  existing_file.txt",
6538            "  new_file_at_root.txt  <== selected  <== marked",
6539        ],
6540        "New file should be created at root level and visible without root prefix"
6541    );
6542
6543    assert!(
6544        fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
6545        "File should be created in the actual root directory"
6546    );
6547
6548    // Test 2: Create new directory when no entry is selected
6549    panel.update(cx, |panel, _| {
6550        panel.state.selection = None;
6551    });
6552
6553    panel.update_in(cx, |panel, window, cx| {
6554        panel.new_directory(&NewDirectory, window, cx);
6555    });
6556    cx.run_until_parked();
6557
6558    panel.update_in(cx, |panel, window, cx| {
6559        assert!(panel.filename_editor.read(cx).is_focused(window));
6560    });
6561
6562    #[rustfmt::skip]
6563    assert_eq!(
6564        visible_entries_as_strings(&panel, 0..20, cx),
6565        &[
6566            "> [EDITOR: '']  <== selected",
6567            "> existing_dir",
6568            "  existing_file.txt",
6569            "  new_file_at_root.txt",
6570        ],
6571        "Directory editor should appear at root level when hide_root=true and no selection"
6572    );
6573
6574    let confirm = panel.update_in(cx, |panel, window, cx| {
6575        panel.filename_editor.update(cx, |editor, cx| {
6576            editor.set_text("new_dir_at_root", window, cx)
6577        });
6578        panel.confirm_edit(true, window, cx).unwrap()
6579    });
6580    confirm.await.unwrap();
6581    cx.run_until_parked();
6582
6583    #[rustfmt::skip]
6584    assert_eq!(
6585        visible_entries_as_strings(&panel, 0..20, cx),
6586        &[
6587            "> existing_dir",
6588            "v new_dir_at_root  <== selected",
6589            "  existing_file.txt",
6590            "  new_file_at_root.txt",
6591        ],
6592        "New directory should be created at root level and visible without root prefix"
6593    );
6594
6595    assert!(
6596        fs.is_dir(Path::new("/root/new_dir_at_root")).await,
6597        "Directory should be created in the actual root directory"
6598    );
6599}
6600
6601#[gpui::test]
6602async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
6603    init_test(cx);
6604
6605    let fs = FakeFs::new(cx.executor());
6606    fs.insert_tree(
6607        "/root",
6608        json!({
6609            "dir1": {
6610                "file1.txt": "",
6611                "dir2": {
6612                    "file2.txt": ""
6613                }
6614            },
6615            "file3.txt": ""
6616        }),
6617    )
6618    .await;
6619
6620    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6621    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6622    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6623    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6624    cx.run_until_parked();
6625
6626    panel.update(cx, |panel, cx| {
6627        let project = panel.project.read(cx);
6628        let worktree = project.visible_worktrees(cx).next().unwrap();
6629        let worktree = worktree.read(cx);
6630
6631        // Test 1: Target is a directory, should highlight the directory itself
6632        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
6633        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
6634        assert_eq!(
6635            result,
6636            Some(dir_entry.id),
6637            "Should highlight directory itself"
6638        );
6639
6640        // Test 2: Target is nested file, should highlight immediate parent
6641        let nested_file = worktree
6642            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
6643            .unwrap();
6644        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
6645        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
6646        assert_eq!(
6647            result,
6648            Some(nested_parent.id),
6649            "Should highlight immediate parent"
6650        );
6651
6652        // Test 3: Target is root level file, should highlight root
6653        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
6654        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
6655        assert_eq!(
6656            result,
6657            Some(worktree.root_entry().unwrap().id),
6658            "Root level file should return None"
6659        );
6660
6661        // Test 4: Target is root itself, should highlight root
6662        let root_entry = worktree.root_entry().unwrap();
6663        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
6664        assert_eq!(
6665            result,
6666            Some(root_entry.id),
6667            "Root level file should return None"
6668        );
6669    });
6670}
6671
6672#[gpui::test]
6673async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
6674    init_test(cx);
6675
6676    let fs = FakeFs::new(cx.executor());
6677    fs.insert_tree(
6678        "/root",
6679        json!({
6680            "parent_dir": {
6681                "child_file.txt": "",
6682                "sibling_file.txt": "",
6683                "child_dir": {
6684                    "nested_file.txt": ""
6685                }
6686            },
6687            "other_dir": {
6688                "other_file.txt": ""
6689            }
6690        }),
6691    )
6692    .await;
6693
6694    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6695    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6696    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6697    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6698    cx.run_until_parked();
6699
6700    panel.update(cx, |panel, cx| {
6701        let project = panel.project.read(cx);
6702        let worktree = project.visible_worktrees(cx).next().unwrap();
6703        let worktree_id = worktree.read(cx).id();
6704        let worktree = worktree.read(cx);
6705
6706        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
6707        let child_file = worktree
6708            .entry_for_path(rel_path("parent_dir/child_file.txt"))
6709            .unwrap();
6710        let sibling_file = worktree
6711            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
6712            .unwrap();
6713        let child_dir = worktree
6714            .entry_for_path(rel_path("parent_dir/child_dir"))
6715            .unwrap();
6716        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
6717        let other_file = worktree
6718            .entry_for_path(rel_path("other_dir/other_file.txt"))
6719            .unwrap();
6720
6721        // Test 1: Single item drag, don't highlight parent directory
6722        let dragged_selection = DraggedSelection {
6723            active_selection: SelectedEntry {
6724                worktree_id,
6725                entry_id: child_file.id,
6726            },
6727            marked_selections: Arc::new([SelectedEntry {
6728                worktree_id,
6729                entry_id: child_file.id,
6730            }]),
6731        };
6732        let result =
6733            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6734        assert_eq!(result, None, "Should not highlight parent of dragged item");
6735
6736        // Test 2: Single item drag, don't highlight sibling files
6737        let result = panel.highlight_entry_for_selection_drag(
6738            sibling_file,
6739            worktree,
6740            &dragged_selection,
6741            cx,
6742        );
6743        assert_eq!(result, None, "Should not highlight sibling files");
6744
6745        // Test 3: Single item drag, highlight unrelated directory
6746        let result =
6747            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
6748        assert_eq!(
6749            result,
6750            Some(other_dir.id),
6751            "Should highlight unrelated directory"
6752        );
6753
6754        // Test 4: Single item drag, highlight sibling directory
6755        let result =
6756            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6757        assert_eq!(
6758            result,
6759            Some(child_dir.id),
6760            "Should highlight sibling directory"
6761        );
6762
6763        // Test 5: Multiple items drag, highlight parent directory
6764        let dragged_selection = DraggedSelection {
6765            active_selection: SelectedEntry {
6766                worktree_id,
6767                entry_id: child_file.id,
6768            },
6769            marked_selections: Arc::new([
6770                SelectedEntry {
6771                    worktree_id,
6772                    entry_id: child_file.id,
6773                },
6774                SelectedEntry {
6775                    worktree_id,
6776                    entry_id: sibling_file.id,
6777                },
6778            ]),
6779        };
6780        let result =
6781            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6782        assert_eq!(
6783            result,
6784            Some(parent_dir.id),
6785            "Should highlight parent with multiple items"
6786        );
6787
6788        // Test 6: Target is file in different directory, highlight parent
6789        let result =
6790            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
6791        assert_eq!(
6792            result,
6793            Some(other_dir.id),
6794            "Should highlight parent of target file"
6795        );
6796
6797        // Test 7: Target is directory, always highlight
6798        let result =
6799            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6800        assert_eq!(
6801            result,
6802            Some(child_dir.id),
6803            "Should always highlight directories"
6804        );
6805    });
6806}
6807
6808#[gpui::test]
6809async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
6810    init_test(cx);
6811
6812    let fs = FakeFs::new(cx.executor());
6813    fs.insert_tree(
6814        "/root1",
6815        json!({
6816            "src": {
6817                "main.rs": "",
6818                "lib.rs": ""
6819            }
6820        }),
6821    )
6822    .await;
6823    fs.insert_tree(
6824        "/root2",
6825        json!({
6826            "src": {
6827                "main.rs": "",
6828                "test.rs": ""
6829            }
6830        }),
6831    )
6832    .await;
6833
6834    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6835    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6836    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6837    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6838    cx.run_until_parked();
6839
6840    panel.update(cx, |panel, cx| {
6841        let project = panel.project.read(cx);
6842        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6843
6844        let worktree_a = &worktrees[0];
6845        let main_rs_from_a = worktree_a
6846            .read(cx)
6847            .entry_for_path(rel_path("src/main.rs"))
6848            .unwrap();
6849
6850        let worktree_b = &worktrees[1];
6851        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
6852        let main_rs_from_b = worktree_b
6853            .read(cx)
6854            .entry_for_path(rel_path("src/main.rs"))
6855            .unwrap();
6856
6857        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
6858        let dragged_selection = DraggedSelection {
6859            active_selection: SelectedEntry {
6860                worktree_id: worktree_a.read(cx).id(),
6861                entry_id: main_rs_from_a.id,
6862            },
6863            marked_selections: Arc::new([SelectedEntry {
6864                worktree_id: worktree_a.read(cx).id(),
6865                entry_id: main_rs_from_a.id,
6866            }]),
6867        };
6868
6869        let result = panel.highlight_entry_for_selection_drag(
6870            src_dir_from_b,
6871            worktree_b.read(cx),
6872            &dragged_selection,
6873            cx,
6874        );
6875        assert_eq!(
6876            result,
6877            Some(src_dir_from_b.id),
6878            "Should highlight target directory from different worktree even with same relative path"
6879        );
6880
6881        // Test dragging file from worktree A onto file with same relative path in worktree B
6882        let result = panel.highlight_entry_for_selection_drag(
6883            main_rs_from_b,
6884            worktree_b.read(cx),
6885            &dragged_selection,
6886            cx,
6887        );
6888        assert_eq!(
6889            result,
6890            Some(src_dir_from_b.id),
6891            "Should highlight parent of target file from different worktree"
6892        );
6893    });
6894}
6895
6896#[gpui::test]
6897async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
6898    init_test(cx);
6899
6900    let fs = FakeFs::new(cx.executor());
6901    fs.insert_tree(
6902        "/root1",
6903        json!({
6904            "parent_dir": {
6905                "child_file.txt": "",
6906                "nested_dir": {
6907                    "nested_file.txt": ""
6908                }
6909            },
6910            "root_file.txt": ""
6911        }),
6912    )
6913    .await;
6914
6915    fs.insert_tree(
6916        "/root2",
6917        json!({
6918            "other_dir": {
6919                "other_file.txt": ""
6920            }
6921        }),
6922    )
6923    .await;
6924
6925    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6926    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6927    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6928    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6929    cx.run_until_parked();
6930
6931    panel.update(cx, |panel, cx| {
6932        let project = panel.project.read(cx);
6933        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6934        let worktree1 = worktrees[0].read(cx);
6935        let worktree2 = worktrees[1].read(cx);
6936        let worktree1_id = worktree1.id();
6937        let _worktree2_id = worktree2.id();
6938
6939        let root1_entry = worktree1.root_entry().unwrap();
6940        let root2_entry = worktree2.root_entry().unwrap();
6941        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
6942        let child_file = worktree1
6943            .entry_for_path(rel_path("parent_dir/child_file.txt"))
6944            .unwrap();
6945        let nested_file = worktree1
6946            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
6947            .unwrap();
6948        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
6949
6950        // Test 1: Multiple entries - should always highlight background
6951        let multiple_dragged_selection = DraggedSelection {
6952            active_selection: SelectedEntry {
6953                worktree_id: worktree1_id,
6954                entry_id: child_file.id,
6955            },
6956            marked_selections: Arc::new([
6957                SelectedEntry {
6958                    worktree_id: worktree1_id,
6959                    entry_id: child_file.id,
6960                },
6961                SelectedEntry {
6962                    worktree_id: worktree1_id,
6963                    entry_id: nested_file.id,
6964                },
6965            ]),
6966        };
6967
6968        let result = panel.should_highlight_background_for_selection_drag(
6969            &multiple_dragged_selection,
6970            root1_entry.id,
6971            cx,
6972        );
6973        assert!(result, "Should highlight background for multiple entries");
6974
6975        // Test 2: Single entry with non-empty parent path - should highlight background
6976        let nested_dragged_selection = DraggedSelection {
6977            active_selection: SelectedEntry {
6978                worktree_id: worktree1_id,
6979                entry_id: nested_file.id,
6980            },
6981            marked_selections: Arc::new([SelectedEntry {
6982                worktree_id: worktree1_id,
6983                entry_id: nested_file.id,
6984            }]),
6985        };
6986
6987        let result = panel.should_highlight_background_for_selection_drag(
6988            &nested_dragged_selection,
6989            root1_entry.id,
6990            cx,
6991        );
6992        assert!(result, "Should highlight background for nested file");
6993
6994        // Test 3: Single entry at root level, same worktree - should NOT highlight background
6995        let root_file_dragged_selection = DraggedSelection {
6996            active_selection: SelectedEntry {
6997                worktree_id: worktree1_id,
6998                entry_id: root_file.id,
6999            },
7000            marked_selections: Arc::new([SelectedEntry {
7001                worktree_id: worktree1_id,
7002                entry_id: root_file.id,
7003            }]),
7004        };
7005
7006        let result = panel.should_highlight_background_for_selection_drag(
7007            &root_file_dragged_selection,
7008            root1_entry.id,
7009            cx,
7010        );
7011        assert!(
7012            !result,
7013            "Should NOT highlight background for root file in same worktree"
7014        );
7015
7016        // Test 4: Single entry at root level, different worktree - should highlight background
7017        let result = panel.should_highlight_background_for_selection_drag(
7018            &root_file_dragged_selection,
7019            root2_entry.id,
7020            cx,
7021        );
7022        assert!(
7023            result,
7024            "Should highlight background for root file from different worktree"
7025        );
7026
7027        // Test 5: Single entry in subdirectory - should highlight background
7028        let child_file_dragged_selection = DraggedSelection {
7029            active_selection: SelectedEntry {
7030                worktree_id: worktree1_id,
7031                entry_id: child_file.id,
7032            },
7033            marked_selections: Arc::new([SelectedEntry {
7034                worktree_id: worktree1_id,
7035                entry_id: child_file.id,
7036            }]),
7037        };
7038
7039        let result = panel.should_highlight_background_for_selection_drag(
7040            &child_file_dragged_selection,
7041            root1_entry.id,
7042            cx,
7043        );
7044        assert!(
7045            result,
7046            "Should highlight background for file with non-empty parent path"
7047        );
7048    });
7049}
7050
7051#[gpui::test]
7052async fn test_hide_root(cx: &mut gpui::TestAppContext) {
7053    init_test(cx);
7054
7055    let fs = FakeFs::new(cx.executor());
7056    fs.insert_tree(
7057        "/root1",
7058        json!({
7059            "dir1": {
7060                "file1.txt": "content",
7061                "file2.txt": "content",
7062            },
7063            "dir2": {
7064                "file3.txt": "content",
7065            },
7066            "file4.txt": "content",
7067        }),
7068    )
7069    .await;
7070
7071    fs.insert_tree(
7072        "/root2",
7073        json!({
7074            "dir3": {
7075                "file5.txt": "content",
7076            },
7077            "file6.txt": "content",
7078        }),
7079    )
7080    .await;
7081
7082    // Test 1: Single worktree with hide_root = false
7083    {
7084        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7085        let workspace =
7086            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7087        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7088
7089        cx.update(|_, cx| {
7090            let settings = *ProjectPanelSettings::get_global(cx);
7091            ProjectPanelSettings::override_global(
7092                ProjectPanelSettings {
7093                    hide_root: false,
7094                    ..settings
7095                },
7096                cx,
7097            );
7098        });
7099
7100        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7101        cx.run_until_parked();
7102
7103        #[rustfmt::skip]
7104        assert_eq!(
7105            visible_entries_as_strings(&panel, 0..10, cx),
7106            &[
7107                "v root1",
7108                "    > dir1",
7109                "    > dir2",
7110                "      file4.txt",
7111            ],
7112            "With hide_root=false and single worktree, root should be visible"
7113        );
7114    }
7115
7116    // Test 2: Single worktree with hide_root = true
7117    {
7118        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7119        let workspace =
7120            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7121        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7122
7123        // Set hide_root to true
7124        cx.update(|_, cx| {
7125            let settings = *ProjectPanelSettings::get_global(cx);
7126            ProjectPanelSettings::override_global(
7127                ProjectPanelSettings {
7128                    hide_root: true,
7129                    ..settings
7130                },
7131                cx,
7132            );
7133        });
7134
7135        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7136        cx.run_until_parked();
7137
7138        assert_eq!(
7139            visible_entries_as_strings(&panel, 0..10, cx),
7140            &["> dir1", "> dir2", "  file4.txt",],
7141            "With hide_root=true and single worktree, root should be hidden"
7142        );
7143
7144        // Test expanding directories still works without root
7145        toggle_expand_dir(&panel, "root1/dir1", cx);
7146        assert_eq!(
7147            visible_entries_as_strings(&panel, 0..10, cx),
7148            &[
7149                "v dir1  <== selected",
7150                "      file1.txt",
7151                "      file2.txt",
7152                "> dir2",
7153                "  file4.txt",
7154            ],
7155            "Should be able to expand directories even when root is hidden"
7156        );
7157    }
7158
7159    // Test 3: Multiple worktrees with hide_root = true
7160    {
7161        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7162        let workspace =
7163            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7164        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7165
7166        // Set hide_root to true
7167        cx.update(|_, cx| {
7168            let settings = *ProjectPanelSettings::get_global(cx);
7169            ProjectPanelSettings::override_global(
7170                ProjectPanelSettings {
7171                    hide_root: true,
7172                    ..settings
7173                },
7174                cx,
7175            );
7176        });
7177
7178        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7179        cx.run_until_parked();
7180
7181        assert_eq!(
7182            visible_entries_as_strings(&panel, 0..10, cx),
7183            &[
7184                "v root1",
7185                "    > dir1",
7186                "    > dir2",
7187                "      file4.txt",
7188                "v root2",
7189                "    > dir3",
7190                "      file6.txt",
7191            ],
7192            "With hide_root=true and multiple worktrees, roots should still be visible"
7193        );
7194    }
7195
7196    // Test 4: Multiple worktrees with hide_root = false
7197    {
7198        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7199        let workspace =
7200            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7201        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7202
7203        cx.update(|_, cx| {
7204            let settings = *ProjectPanelSettings::get_global(cx);
7205            ProjectPanelSettings::override_global(
7206                ProjectPanelSettings {
7207                    hide_root: false,
7208                    ..settings
7209                },
7210                cx,
7211            );
7212        });
7213
7214        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7215        cx.run_until_parked();
7216
7217        assert_eq!(
7218            visible_entries_as_strings(&panel, 0..10, cx),
7219            &[
7220                "v root1",
7221                "    > dir1",
7222                "    > dir2",
7223                "      file4.txt",
7224                "v root2",
7225                "    > dir3",
7226                "      file6.txt",
7227            ],
7228            "With hide_root=false and multiple worktrees, roots should be visible"
7229        );
7230    }
7231}
7232
7233#[gpui::test]
7234async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
7235    init_test_with_editor(cx);
7236
7237    let fs = FakeFs::new(cx.executor());
7238    fs.insert_tree(
7239        "/root",
7240        json!({
7241            "file1.txt": "content of file1",
7242            "file2.txt": "content of file2",
7243            "dir1": {
7244                "file3.txt": "content of file3"
7245            }
7246        }),
7247    )
7248    .await;
7249
7250    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7251    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7252    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7253    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7254    cx.run_until_parked();
7255
7256    let file1_path = "root/file1.txt";
7257    let file2_path = "root/file2.txt";
7258    select_path_with_mark(&panel, file1_path, cx);
7259    select_path_with_mark(&panel, file2_path, cx);
7260
7261    panel.update_in(cx, |panel, window, cx| {
7262        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
7263    });
7264    cx.executor().run_until_parked();
7265
7266    workspace
7267        .update(cx, |workspace, _, cx| {
7268            let active_items = workspace
7269                .panes()
7270                .iter()
7271                .filter_map(|pane| pane.read(cx).active_item())
7272                .collect::<Vec<_>>();
7273            assert_eq!(active_items.len(), 1);
7274            let diff_view = active_items
7275                .into_iter()
7276                .next()
7277                .unwrap()
7278                .downcast::<FileDiffView>()
7279                .expect("Open item should be an FileDiffView");
7280            assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
7281            assert_eq!(
7282                diff_view.tab_tooltip_text(cx).unwrap(),
7283                format!(
7284                    "{}{}",
7285                    rel_path(file1_path).display(PathStyle::local()),
7286                    rel_path(file2_path).display(PathStyle::local())
7287                )
7288            );
7289        })
7290        .unwrap();
7291
7292    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
7293    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
7294    let worktree_id = panel.update(cx, |panel, cx| {
7295        panel
7296            .project
7297            .read(cx)
7298            .worktrees(cx)
7299            .next()
7300            .unwrap()
7301            .read(cx)
7302            .id()
7303    });
7304
7305    let expected_entries = [
7306        SelectedEntry {
7307            worktree_id,
7308            entry_id: file1_entry_id,
7309        },
7310        SelectedEntry {
7311            worktree_id,
7312            entry_id: file2_entry_id,
7313        },
7314    ];
7315    panel.update(cx, |panel, _cx| {
7316        assert_eq!(
7317            &panel.marked_entries, &expected_entries,
7318            "Should keep marked entries after comparison"
7319        );
7320    });
7321
7322    panel.update(cx, |panel, cx| {
7323        panel.project.update(cx, |_, cx| {
7324            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
7325        })
7326    });
7327
7328    panel.update(cx, |panel, _cx| {
7329        assert_eq!(
7330            &panel.marked_entries, &expected_entries,
7331            "Marked entries should persist after focusing back on the project panel"
7332        );
7333    });
7334}
7335
7336#[gpui::test]
7337async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
7338    init_test_with_editor(cx);
7339
7340    let fs = FakeFs::new(cx.executor());
7341    fs.insert_tree(
7342        "/root",
7343        json!({
7344            "file1.txt": "content of file1",
7345            "file2.txt": "content of file2",
7346            "dir1": {},
7347            "dir2": {
7348                "file3.txt": "content of file3"
7349            }
7350        }),
7351    )
7352    .await;
7353
7354    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7355    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7356    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7357    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7358    cx.run_until_parked();
7359
7360    // Test 1: When only one file is selected, there should be no compare option
7361    select_path(&panel, "root/file1.txt", cx);
7362
7363    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7364    assert_eq!(
7365        selected_files, None,
7366        "Should not have compare option when only one file is selected"
7367    );
7368
7369    // Test 2: When multiple files are selected, there should be a compare option
7370    select_path_with_mark(&panel, "root/file1.txt", cx);
7371    select_path_with_mark(&panel, "root/file2.txt", cx);
7372
7373    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7374    assert!(
7375        selected_files.is_some(),
7376        "Should have files selected for comparison"
7377    );
7378    if let Some((file1, file2)) = selected_files {
7379        assert!(
7380            file1.to_string_lossy().ends_with("file1.txt")
7381                && file2.to_string_lossy().ends_with("file2.txt"),
7382            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
7383        );
7384    }
7385
7386    // Test 3: Selecting a directory shouldn't count as a comparable file
7387    select_path_with_mark(&panel, "root/dir1", cx);
7388
7389    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7390    assert!(
7391        selected_files.is_some(),
7392        "Directory selection should not affect comparable files"
7393    );
7394    if let Some((file1, file2)) = selected_files {
7395        assert!(
7396            file1.to_string_lossy().ends_with("file1.txt")
7397                && file2.to_string_lossy().ends_with("file2.txt"),
7398            "Selecting a directory should not affect the number of comparable files"
7399        );
7400    }
7401
7402    // Test 4: Selecting one more file
7403    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
7404
7405    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7406    assert!(
7407        selected_files.is_some(),
7408        "Directory selection should not affect comparable files"
7409    );
7410    if let Some((file1, file2)) = selected_files {
7411        assert!(
7412            file1.to_string_lossy().ends_with("file2.txt")
7413                && file2.to_string_lossy().ends_with("file3.txt"),
7414            "Selecting a directory should not affect the number of comparable files"
7415        );
7416    }
7417}
7418
7419#[gpui::test]
7420async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
7421    init_test(cx);
7422
7423    let fs = FakeFs::new(cx.executor());
7424    fs.insert_tree(
7425        "/root",
7426        json!({
7427            ".hidden-file.txt": "hidden file content",
7428            "visible-file.txt": "visible file content",
7429            ".hidden-parent-dir": {
7430                "nested-dir": {
7431                    "file.txt": "file content",
7432                }
7433            },
7434            "visible-dir": {
7435                "file-in-visible.txt": "file content",
7436                "nested": {
7437                    ".hidden-nested-dir": {
7438                        ".double-hidden-dir": {
7439                            "deep-file-1.txt": "deep content 1",
7440                            "deep-file-2.txt": "deep content 2"
7441                        },
7442                        "hidden-nested-file-1.txt": "hidden nested 1",
7443                        "hidden-nested-file-2.txt": "hidden nested 2"
7444                    },
7445                    "visible-nested-file.txt": "visible nested content"
7446                }
7447            }
7448        }),
7449    )
7450    .await;
7451
7452    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7453    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7454    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7455
7456    cx.update(|_, cx| {
7457        let settings = *ProjectPanelSettings::get_global(cx);
7458        ProjectPanelSettings::override_global(
7459            ProjectPanelSettings {
7460                hide_hidden: false,
7461                ..settings
7462            },
7463            cx,
7464        );
7465    });
7466
7467    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7468    cx.run_until_parked();
7469
7470    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
7471    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
7472    toggle_expand_dir(&panel, "root/visible-dir", cx);
7473    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
7474    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
7475    toggle_expand_dir(
7476        &panel,
7477        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
7478        cx,
7479    );
7480
7481    let expanded = [
7482        "v root",
7483        "    v .hidden-parent-dir",
7484        "        v nested-dir",
7485        "              file.txt",
7486        "    v visible-dir",
7487        "        v nested",
7488        "            v .hidden-nested-dir",
7489        "                v .double-hidden-dir  <== selected",
7490        "                      deep-file-1.txt",
7491        "                      deep-file-2.txt",
7492        "                  hidden-nested-file-1.txt",
7493        "                  hidden-nested-file-2.txt",
7494        "              visible-nested-file.txt",
7495        "          file-in-visible.txt",
7496        "      .hidden-file.txt",
7497        "      visible-file.txt",
7498    ];
7499
7500    assert_eq!(
7501        visible_entries_as_strings(&panel, 0..30, cx),
7502        &expanded,
7503        "With hide_hidden=false, contents of hidden nested directory should be visible"
7504    );
7505
7506    cx.update(|_, cx| {
7507        let settings = *ProjectPanelSettings::get_global(cx);
7508        ProjectPanelSettings::override_global(
7509            ProjectPanelSettings {
7510                hide_hidden: true,
7511                ..settings
7512            },
7513            cx,
7514        );
7515    });
7516
7517    panel.update_in(cx, |panel, window, cx| {
7518        panel.update_visible_entries(None, None, false, window, cx);
7519    });
7520    cx.run_until_parked();
7521
7522    assert_eq!(
7523        visible_entries_as_strings(&panel, 0..30, cx),
7524        &[
7525            "v root",
7526            "    v visible-dir",
7527            "        v nested",
7528            "              visible-nested-file.txt",
7529            "          file-in-visible.txt",
7530            "      visible-file.txt",
7531        ],
7532        "With hide_hidden=false, contents of hidden nested directory should be visible"
7533    );
7534
7535    panel.update_in(cx, |panel, window, cx| {
7536        let settings = *ProjectPanelSettings::get_global(cx);
7537        ProjectPanelSettings::override_global(
7538            ProjectPanelSettings {
7539                hide_hidden: false,
7540                ..settings
7541            },
7542            cx,
7543        );
7544        panel.update_visible_entries(None, None, false, window, cx);
7545    });
7546    cx.run_until_parked();
7547
7548    assert_eq!(
7549        visible_entries_as_strings(&panel, 0..30, cx),
7550        &expanded,
7551        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
7552    );
7553}
7554
7555fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7556    let path = rel_path(path);
7557    panel.update_in(cx, |panel, window, cx| {
7558        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7559            let worktree = worktree.read(cx);
7560            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7561                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7562                panel.update_visible_entries(
7563                    Some((worktree.id(), entry_id)),
7564                    None,
7565                    false,
7566                    window,
7567                    cx,
7568                );
7569                return;
7570            }
7571        }
7572        panic!("no worktree for path {:?}", path);
7573    });
7574    cx.run_until_parked();
7575}
7576
7577fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7578    let path = rel_path(path);
7579    panel.update(cx, |panel, cx| {
7580        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7581            let worktree = worktree.read(cx);
7582            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7583                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7584                let entry = crate::SelectedEntry {
7585                    worktree_id: worktree.id(),
7586                    entry_id,
7587                };
7588                if !panel.marked_entries.contains(&entry) {
7589                    panel.marked_entries.push(entry);
7590                }
7591                panel.state.selection = Some(entry);
7592                return;
7593            }
7594        }
7595        panic!("no worktree for path {:?}", path);
7596    });
7597}
7598
7599fn drag_selection_to(
7600    panel: &Entity<ProjectPanel>,
7601    target_path: &str,
7602    is_file: bool,
7603    cx: &mut VisualTestContext,
7604) {
7605    let target_entry = find_project_entry(panel, target_path, cx)
7606        .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
7607
7608    panel.update_in(cx, |panel, window, cx| {
7609        let selection = panel
7610            .state
7611            .selection
7612            .expect("a selection is required before dragging");
7613        let drag = DraggedSelection {
7614            active_selection: SelectedEntry {
7615                worktree_id: selection.worktree_id,
7616                entry_id: panel.resolve_entry(selection.entry_id),
7617            },
7618            marked_selections: Arc::from(panel.marked_entries.clone()),
7619        };
7620        panel.drag_onto(&drag, target_entry, is_file, window, cx);
7621    });
7622    cx.executor().run_until_parked();
7623}
7624
7625fn find_project_entry(
7626    panel: &Entity<ProjectPanel>,
7627    path: &str,
7628    cx: &mut VisualTestContext,
7629) -> Option<ProjectEntryId> {
7630    let path = rel_path(path);
7631    panel.update(cx, |panel, cx| {
7632        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7633            let worktree = worktree.read(cx);
7634            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7635                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
7636            }
7637        }
7638        panic!("no worktree for path {path:?}");
7639    })
7640}
7641
7642fn visible_entries_as_strings(
7643    panel: &Entity<ProjectPanel>,
7644    range: Range<usize>,
7645    cx: &mut VisualTestContext,
7646) -> Vec<String> {
7647    let mut result = Vec::new();
7648    let mut project_entries = HashSet::default();
7649    let mut has_editor = false;
7650
7651    panel.update_in(cx, |panel, window, cx| {
7652        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
7653            if details.is_editing {
7654                assert!(!has_editor, "duplicate editor entry");
7655                has_editor = true;
7656            } else {
7657                assert!(
7658                    project_entries.insert(project_entry),
7659                    "duplicate project entry {:?} {:?}",
7660                    project_entry,
7661                    details
7662                );
7663            }
7664
7665            let indent = "    ".repeat(details.depth);
7666            let icon = if details.kind.is_dir() {
7667                if details.is_expanded { "v " } else { "> " }
7668            } else {
7669                "  "
7670            };
7671            #[cfg(windows)]
7672            let filename = details.filename.replace("\\", "/");
7673            #[cfg(not(windows))]
7674            let filename = details.filename;
7675            let name = if details.is_editing {
7676                format!("[EDITOR: '{}']", filename)
7677            } else if details.is_processing {
7678                format!("[PROCESSING: '{}']", filename)
7679            } else {
7680                filename
7681            };
7682            let selected = if details.is_selected {
7683                "  <== selected"
7684            } else {
7685                ""
7686            };
7687            let marked = if details.is_marked {
7688                "  <== marked"
7689            } else {
7690                ""
7691            };
7692
7693            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
7694        });
7695    });
7696
7697    result
7698}
7699
7700fn init_test(cx: &mut TestAppContext) {
7701    cx.update(|cx| {
7702        let settings_store = SettingsStore::test(cx);
7703        cx.set_global(settings_store);
7704        theme::init(theme::LoadThemes::JustBase, cx);
7705        crate::init(cx);
7706
7707        cx.update_global::<SettingsStore, _>(|store, cx| {
7708            store.update_user_settings(cx, |settings| {
7709                settings
7710                    .project_panel
7711                    .get_or_insert_default()
7712                    .auto_fold_dirs = Some(false);
7713                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
7714            });
7715        });
7716    });
7717}
7718
7719fn init_test_with_editor(cx: &mut TestAppContext) {
7720    cx.update(|cx| {
7721        let app_state = AppState::test(cx);
7722        theme::init(theme::LoadThemes::JustBase, cx);
7723        editor::init(cx);
7724        crate::init(cx);
7725        workspace::init(app_state, cx);
7726
7727        cx.update_global::<SettingsStore, _>(|store, cx| {
7728            store.update_user_settings(cx, |settings| {
7729                settings
7730                    .project_panel
7731                    .get_or_insert_default()
7732                    .auto_fold_dirs = Some(false);
7733                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
7734            });
7735        });
7736    });
7737}
7738
7739fn set_auto_open_settings(
7740    cx: &mut TestAppContext,
7741    auto_open_settings: ProjectPanelAutoOpenSettings,
7742) {
7743    cx.update(|cx| {
7744        cx.update_global::<SettingsStore, _>(|store, cx| {
7745            store.update_user_settings(cx, |settings| {
7746                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
7747            });
7748        })
7749    });
7750}
7751
7752fn ensure_single_file_is_opened(
7753    window: &WindowHandle<Workspace>,
7754    expected_path: &str,
7755    cx: &mut TestAppContext,
7756) {
7757    window
7758        .update(cx, |workspace, _, cx| {
7759            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
7760            assert_eq!(worktrees.len(), 1);
7761            let worktree_id = worktrees[0].read(cx).id();
7762
7763            let open_project_paths = workspace
7764                .panes()
7765                .iter()
7766                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
7767                .collect::<Vec<_>>();
7768            assert_eq!(
7769                open_project_paths,
7770                vec![ProjectPath {
7771                    worktree_id,
7772                    path: Arc::from(rel_path(expected_path))
7773                }],
7774                "Should have opened file, selected in project panel"
7775            );
7776        })
7777        .unwrap();
7778}
7779
7780fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
7781    assert!(
7782        !cx.has_pending_prompt(),
7783        "Should have no prompts before the deletion"
7784    );
7785    panel.update_in(cx, |panel, window, cx| {
7786        panel.delete(&Delete { skip_prompt: false }, window, cx)
7787    });
7788    assert!(
7789        cx.has_pending_prompt(),
7790        "Should have a prompt after the deletion"
7791    );
7792    cx.simulate_prompt_answer("Delete");
7793    assert!(
7794        !cx.has_pending_prompt(),
7795        "Should have no prompts after prompt was replied to"
7796    );
7797    cx.executor().run_until_parked();
7798}
7799
7800fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
7801    assert!(
7802        !cx.has_pending_prompt(),
7803        "Should have no prompts before the deletion"
7804    );
7805    panel.update_in(cx, |panel, window, cx| {
7806        panel.delete(&Delete { skip_prompt: true }, window, cx)
7807    });
7808    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
7809    cx.executor().run_until_parked();
7810}
7811
7812fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
7813    assert!(
7814        !cx.has_pending_prompt(),
7815        "Should have no prompts after deletion operation closes the file"
7816    );
7817    workspace
7818        .read_with(cx, |workspace, cx| {
7819            let open_project_paths = workspace
7820                .panes()
7821                .iter()
7822                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
7823                .collect::<Vec<_>>();
7824            assert!(
7825                open_project_paths.is_empty(),
7826                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
7827            );
7828        })
7829        .unwrap();
7830}
7831
7832struct TestProjectItemView {
7833    focus_handle: FocusHandle,
7834    path: ProjectPath,
7835}
7836
7837struct TestProjectItem {
7838    path: ProjectPath,
7839}
7840
7841impl project::ProjectItem for TestProjectItem {
7842    fn try_open(
7843        _project: &Entity<Project>,
7844        path: &ProjectPath,
7845        cx: &mut App,
7846    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
7847        let path = path.clone();
7848        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
7849    }
7850
7851    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
7852        None
7853    }
7854
7855    fn project_path(&self, _: &App) -> Option<ProjectPath> {
7856        Some(self.path.clone())
7857    }
7858
7859    fn is_dirty(&self) -> bool {
7860        false
7861    }
7862}
7863
7864impl ProjectItem for TestProjectItemView {
7865    type Item = TestProjectItem;
7866
7867    fn for_project_item(
7868        _: Entity<Project>,
7869        _: Option<&Pane>,
7870        project_item: Entity<Self::Item>,
7871        _: &mut Window,
7872        cx: &mut Context<Self>,
7873    ) -> Self
7874    where
7875        Self: Sized,
7876    {
7877        Self {
7878            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
7879            focus_handle: cx.focus_handle(),
7880        }
7881    }
7882}
7883
7884impl Item for TestProjectItemView {
7885    type Event = ();
7886
7887    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
7888        "Test".into()
7889    }
7890}
7891
7892impl EventEmitter<()> for TestProjectItemView {}
7893
7894impl Focusable for TestProjectItemView {
7895    fn focus_handle(&self, _: &App) -> FocusHandle {
7896        self.focus_handle.clone()
7897    }
7898}
7899
7900impl Render for TestProjectItemView {
7901    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7902        Empty
7903    }
7904}