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