project_panel_tests.rs

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