project_panel_tests.rs

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