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#[cfg(windows)]
6666#[gpui::test]
6667async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) {
6668    init_test(cx);
6669
6670    let fs = FakeFs::new(cx.executor());
6671    fs.insert_tree(
6672        path!("/root"),
6673        json!({
6674            "dir1": {
6675                "file1.txt": "",
6676            },
6677        }),
6678    )
6679    .await;
6680
6681    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6682    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6683    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6684
6685    let panel = workspace
6686        .update(cx, |workspace, window, cx| {
6687            let panel = ProjectPanel::new(workspace, window, cx);
6688            workspace.add_panel(panel.clone(), window, cx);
6689            panel
6690        })
6691        .unwrap();
6692    cx.run_until_parked();
6693
6694    #[rustfmt::skip]
6695    assert_eq!(
6696        visible_entries_as_strings(&panel, 0..20, cx),
6697        &[
6698            "v root",
6699            "    > dir1",
6700        ],
6701        "Initial state with nothing selected"
6702    );
6703
6704    panel.update_in(cx, |panel, window, cx| {
6705        panel.new_file(&NewFile, window, cx);
6706    });
6707    cx.run_until_parked();
6708    panel.update_in(cx, |panel, window, cx| {
6709        assert!(panel.filename_editor.read(cx).is_focused(window));
6710    });
6711    panel
6712        .update_in(cx, |panel, window, cx| {
6713            panel
6714                .filename_editor
6715                .update(cx, |editor, cx| editor.set_text("foo.", window, cx));
6716            panel.confirm_edit(true, window, cx).unwrap()
6717        })
6718        .await
6719        .unwrap();
6720    cx.run_until_parked();
6721    #[rustfmt::skip]
6722    assert_eq!(
6723        visible_entries_as_strings(&panel, 0..20, cx),
6724        &[
6725            "v root",
6726            "    > dir1",
6727            "      foo  <== selected  <== marked",
6728        ],
6729        "A new file is created under the root directory without the trailing dot"
6730    );
6731}
6732
6733#[gpui::test]
6734async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
6735    init_test(cx);
6736
6737    let fs = FakeFs::new(cx.executor());
6738    fs.insert_tree(
6739        "/root",
6740        json!({
6741            "dir1": {
6742                "file1.txt": "",
6743                "dir2": {
6744                    "file2.txt": ""
6745                }
6746            },
6747            "file3.txt": ""
6748        }),
6749    )
6750    .await;
6751
6752    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6753    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6754    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6755    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6756    cx.run_until_parked();
6757
6758    panel.update(cx, |panel, cx| {
6759        let project = panel.project.read(cx);
6760        let worktree = project.visible_worktrees(cx).next().unwrap();
6761        let worktree = worktree.read(cx);
6762
6763        // Test 1: Target is a directory, should highlight the directory itself
6764        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
6765        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
6766        assert_eq!(
6767            result,
6768            Some(dir_entry.id),
6769            "Should highlight directory itself"
6770        );
6771
6772        // Test 2: Target is nested file, should highlight immediate parent
6773        let nested_file = worktree
6774            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
6775            .unwrap();
6776        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
6777        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
6778        assert_eq!(
6779            result,
6780            Some(nested_parent.id),
6781            "Should highlight immediate parent"
6782        );
6783
6784        // Test 3: Target is root level file, should highlight root
6785        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
6786        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
6787        assert_eq!(
6788            result,
6789            Some(worktree.root_entry().unwrap().id),
6790            "Root level file should return None"
6791        );
6792
6793        // Test 4: Target is root itself, should highlight root
6794        let root_entry = worktree.root_entry().unwrap();
6795        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
6796        assert_eq!(
6797            result,
6798            Some(root_entry.id),
6799            "Root level file should return None"
6800        );
6801    });
6802}
6803
6804#[gpui::test]
6805async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
6806    init_test(cx);
6807
6808    let fs = FakeFs::new(cx.executor());
6809    fs.insert_tree(
6810        "/root",
6811        json!({
6812            "parent_dir": {
6813                "child_file.txt": "",
6814                "sibling_file.txt": "",
6815                "child_dir": {
6816                    "nested_file.txt": ""
6817                }
6818            },
6819            "other_dir": {
6820                "other_file.txt": ""
6821            }
6822        }),
6823    )
6824    .await;
6825
6826    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6827    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6828    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6829    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6830    cx.run_until_parked();
6831
6832    panel.update(cx, |panel, cx| {
6833        let project = panel.project.read(cx);
6834        let worktree = project.visible_worktrees(cx).next().unwrap();
6835        let worktree_id = worktree.read(cx).id();
6836        let worktree = worktree.read(cx);
6837
6838        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
6839        let child_file = worktree
6840            .entry_for_path(rel_path("parent_dir/child_file.txt"))
6841            .unwrap();
6842        let sibling_file = worktree
6843            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
6844            .unwrap();
6845        let child_dir = worktree
6846            .entry_for_path(rel_path("parent_dir/child_dir"))
6847            .unwrap();
6848        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
6849        let other_file = worktree
6850            .entry_for_path(rel_path("other_dir/other_file.txt"))
6851            .unwrap();
6852
6853        // Test 1: Single item drag, don't highlight parent directory
6854        let dragged_selection = DraggedSelection {
6855            active_selection: SelectedEntry {
6856                worktree_id,
6857                entry_id: child_file.id,
6858            },
6859            marked_selections: Arc::new([SelectedEntry {
6860                worktree_id,
6861                entry_id: child_file.id,
6862            }]),
6863        };
6864        let result =
6865            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6866        assert_eq!(result, None, "Should not highlight parent of dragged item");
6867
6868        // Test 2: Single item drag, don't highlight sibling files
6869        let result = panel.highlight_entry_for_selection_drag(
6870            sibling_file,
6871            worktree,
6872            &dragged_selection,
6873            cx,
6874        );
6875        assert_eq!(result, None, "Should not highlight sibling files");
6876
6877        // Test 3: Single item drag, highlight unrelated directory
6878        let result =
6879            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
6880        assert_eq!(
6881            result,
6882            Some(other_dir.id),
6883            "Should highlight unrelated directory"
6884        );
6885
6886        // Test 4: Single item drag, highlight sibling directory
6887        let result =
6888            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6889        assert_eq!(
6890            result,
6891            Some(child_dir.id),
6892            "Should highlight sibling directory"
6893        );
6894
6895        // Test 5: Multiple items drag, highlight parent directory
6896        let dragged_selection = DraggedSelection {
6897            active_selection: SelectedEntry {
6898                worktree_id,
6899                entry_id: child_file.id,
6900            },
6901            marked_selections: Arc::new([
6902                SelectedEntry {
6903                    worktree_id,
6904                    entry_id: child_file.id,
6905                },
6906                SelectedEntry {
6907                    worktree_id,
6908                    entry_id: sibling_file.id,
6909                },
6910            ]),
6911        };
6912        let result =
6913            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6914        assert_eq!(
6915            result,
6916            Some(parent_dir.id),
6917            "Should highlight parent with multiple items"
6918        );
6919
6920        // Test 6: Target is file in different directory, highlight parent
6921        let result =
6922            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
6923        assert_eq!(
6924            result,
6925            Some(other_dir.id),
6926            "Should highlight parent of target file"
6927        );
6928
6929        // Test 7: Target is directory, always highlight
6930        let result =
6931            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6932        assert_eq!(
6933            result,
6934            Some(child_dir.id),
6935            "Should always highlight directories"
6936        );
6937    });
6938}
6939
6940#[gpui::test]
6941async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
6942    init_test(cx);
6943
6944    let fs = FakeFs::new(cx.executor());
6945    fs.insert_tree(
6946        "/root1",
6947        json!({
6948            "src": {
6949                "main.rs": "",
6950                "lib.rs": ""
6951            }
6952        }),
6953    )
6954    .await;
6955    fs.insert_tree(
6956        "/root2",
6957        json!({
6958            "src": {
6959                "main.rs": "",
6960                "test.rs": ""
6961            }
6962        }),
6963    )
6964    .await;
6965
6966    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6967    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6968    let cx = &mut VisualTestContext::from_window(*workspace, cx);
6969    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6970    cx.run_until_parked();
6971
6972    panel.update(cx, |panel, cx| {
6973        let project = panel.project.read(cx);
6974        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6975
6976        let worktree_a = &worktrees[0];
6977        let main_rs_from_a = worktree_a
6978            .read(cx)
6979            .entry_for_path(rel_path("src/main.rs"))
6980            .unwrap();
6981
6982        let worktree_b = &worktrees[1];
6983        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
6984        let main_rs_from_b = worktree_b
6985            .read(cx)
6986            .entry_for_path(rel_path("src/main.rs"))
6987            .unwrap();
6988
6989        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
6990        let dragged_selection = DraggedSelection {
6991            active_selection: SelectedEntry {
6992                worktree_id: worktree_a.read(cx).id(),
6993                entry_id: main_rs_from_a.id,
6994            },
6995            marked_selections: Arc::new([SelectedEntry {
6996                worktree_id: worktree_a.read(cx).id(),
6997                entry_id: main_rs_from_a.id,
6998            }]),
6999        };
7000
7001        let result = panel.highlight_entry_for_selection_drag(
7002            src_dir_from_b,
7003            worktree_b.read(cx),
7004            &dragged_selection,
7005            cx,
7006        );
7007        assert_eq!(
7008            result,
7009            Some(src_dir_from_b.id),
7010            "Should highlight target directory from different worktree even with same relative path"
7011        );
7012
7013        // Test dragging file from worktree A onto file with same relative path in worktree B
7014        let result = panel.highlight_entry_for_selection_drag(
7015            main_rs_from_b,
7016            worktree_b.read(cx),
7017            &dragged_selection,
7018            cx,
7019        );
7020        assert_eq!(
7021            result,
7022            Some(src_dir_from_b.id),
7023            "Should highlight parent of target file from different worktree"
7024        );
7025    });
7026}
7027
7028#[gpui::test]
7029async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
7030    init_test(cx);
7031
7032    let fs = FakeFs::new(cx.executor());
7033    fs.insert_tree(
7034        "/root1",
7035        json!({
7036            "parent_dir": {
7037                "child_file.txt": "",
7038                "nested_dir": {
7039                    "nested_file.txt": ""
7040                }
7041            },
7042            "root_file.txt": ""
7043        }),
7044    )
7045    .await;
7046
7047    fs.insert_tree(
7048        "/root2",
7049        json!({
7050            "other_dir": {
7051                "other_file.txt": ""
7052            }
7053        }),
7054    )
7055    .await;
7056
7057    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7058    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7059    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7060    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7061    cx.run_until_parked();
7062
7063    panel.update(cx, |panel, cx| {
7064        let project = panel.project.read(cx);
7065        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
7066        let worktree1 = worktrees[0].read(cx);
7067        let worktree2 = worktrees[1].read(cx);
7068        let worktree1_id = worktree1.id();
7069        let _worktree2_id = worktree2.id();
7070
7071        let root1_entry = worktree1.root_entry().unwrap();
7072        let root2_entry = worktree2.root_entry().unwrap();
7073        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
7074        let child_file = worktree1
7075            .entry_for_path(rel_path("parent_dir/child_file.txt"))
7076            .unwrap();
7077        let nested_file = worktree1
7078            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
7079            .unwrap();
7080        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
7081
7082        // Test 1: Multiple entries - should always highlight background
7083        let multiple_dragged_selection = DraggedSelection {
7084            active_selection: SelectedEntry {
7085                worktree_id: worktree1_id,
7086                entry_id: child_file.id,
7087            },
7088            marked_selections: Arc::new([
7089                SelectedEntry {
7090                    worktree_id: worktree1_id,
7091                    entry_id: child_file.id,
7092                },
7093                SelectedEntry {
7094                    worktree_id: worktree1_id,
7095                    entry_id: nested_file.id,
7096                },
7097            ]),
7098        };
7099
7100        let result = panel.should_highlight_background_for_selection_drag(
7101            &multiple_dragged_selection,
7102            root1_entry.id,
7103            cx,
7104        );
7105        assert!(result, "Should highlight background for multiple entries");
7106
7107        // Test 2: Single entry with non-empty parent path - should highlight background
7108        let nested_dragged_selection = DraggedSelection {
7109            active_selection: SelectedEntry {
7110                worktree_id: worktree1_id,
7111                entry_id: nested_file.id,
7112            },
7113            marked_selections: Arc::new([SelectedEntry {
7114                worktree_id: worktree1_id,
7115                entry_id: nested_file.id,
7116            }]),
7117        };
7118
7119        let result = panel.should_highlight_background_for_selection_drag(
7120            &nested_dragged_selection,
7121            root1_entry.id,
7122            cx,
7123        );
7124        assert!(result, "Should highlight background for nested file");
7125
7126        // Test 3: Single entry at root level, same worktree - should NOT highlight background
7127        let root_file_dragged_selection = DraggedSelection {
7128            active_selection: SelectedEntry {
7129                worktree_id: worktree1_id,
7130                entry_id: root_file.id,
7131            },
7132            marked_selections: Arc::new([SelectedEntry {
7133                worktree_id: worktree1_id,
7134                entry_id: root_file.id,
7135            }]),
7136        };
7137
7138        let result = panel.should_highlight_background_for_selection_drag(
7139            &root_file_dragged_selection,
7140            root1_entry.id,
7141            cx,
7142        );
7143        assert!(
7144            !result,
7145            "Should NOT highlight background for root file in same worktree"
7146        );
7147
7148        // Test 4: Single entry at root level, different worktree - should highlight background
7149        let result = panel.should_highlight_background_for_selection_drag(
7150            &root_file_dragged_selection,
7151            root2_entry.id,
7152            cx,
7153        );
7154        assert!(
7155            result,
7156            "Should highlight background for root file from different worktree"
7157        );
7158
7159        // Test 5: Single entry in subdirectory - should highlight background
7160        let child_file_dragged_selection = DraggedSelection {
7161            active_selection: SelectedEntry {
7162                worktree_id: worktree1_id,
7163                entry_id: child_file.id,
7164            },
7165            marked_selections: Arc::new([SelectedEntry {
7166                worktree_id: worktree1_id,
7167                entry_id: child_file.id,
7168            }]),
7169        };
7170
7171        let result = panel.should_highlight_background_for_selection_drag(
7172            &child_file_dragged_selection,
7173            root1_entry.id,
7174            cx,
7175        );
7176        assert!(
7177            result,
7178            "Should highlight background for file with non-empty parent path"
7179        );
7180    });
7181}
7182
7183#[gpui::test]
7184async fn test_hide_root(cx: &mut gpui::TestAppContext) {
7185    init_test(cx);
7186
7187    let fs = FakeFs::new(cx.executor());
7188    fs.insert_tree(
7189        "/root1",
7190        json!({
7191            "dir1": {
7192                "file1.txt": "content",
7193                "file2.txt": "content",
7194            },
7195            "dir2": {
7196                "file3.txt": "content",
7197            },
7198            "file4.txt": "content",
7199        }),
7200    )
7201    .await;
7202
7203    fs.insert_tree(
7204        "/root2",
7205        json!({
7206            "dir3": {
7207                "file5.txt": "content",
7208            },
7209            "file6.txt": "content",
7210        }),
7211    )
7212    .await;
7213
7214    // Test 1: Single worktree with hide_root = false
7215    {
7216        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7217        let workspace =
7218            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7219        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7220
7221        cx.update(|_, cx| {
7222            let settings = *ProjectPanelSettings::get_global(cx);
7223            ProjectPanelSettings::override_global(
7224                ProjectPanelSettings {
7225                    hide_root: false,
7226                    ..settings
7227                },
7228                cx,
7229            );
7230        });
7231
7232        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7233        cx.run_until_parked();
7234
7235        #[rustfmt::skip]
7236        assert_eq!(
7237            visible_entries_as_strings(&panel, 0..10, cx),
7238            &[
7239                "v root1",
7240                "    > dir1",
7241                "    > dir2",
7242                "      file4.txt",
7243            ],
7244            "With hide_root=false and single worktree, root should be visible"
7245        );
7246    }
7247
7248    // Test 2: Single worktree with hide_root = true
7249    {
7250        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7251        let workspace =
7252            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7253        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7254
7255        // Set hide_root to true
7256        cx.update(|_, cx| {
7257            let settings = *ProjectPanelSettings::get_global(cx);
7258            ProjectPanelSettings::override_global(
7259                ProjectPanelSettings {
7260                    hide_root: true,
7261                    ..settings
7262                },
7263                cx,
7264            );
7265        });
7266
7267        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7268        cx.run_until_parked();
7269
7270        assert_eq!(
7271            visible_entries_as_strings(&panel, 0..10, cx),
7272            &["> dir1", "> dir2", "  file4.txt",],
7273            "With hide_root=true and single worktree, root should be hidden"
7274        );
7275
7276        // Test expanding directories still works without root
7277        toggle_expand_dir(&panel, "root1/dir1", cx);
7278        assert_eq!(
7279            visible_entries_as_strings(&panel, 0..10, cx),
7280            &[
7281                "v dir1  <== selected",
7282                "      file1.txt",
7283                "      file2.txt",
7284                "> dir2",
7285                "  file4.txt",
7286            ],
7287            "Should be able to expand directories even when root is hidden"
7288        );
7289    }
7290
7291    // Test 3: Multiple worktrees with hide_root = true
7292    {
7293        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7294        let workspace =
7295            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7296        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7297
7298        // Set hide_root to true
7299        cx.update(|_, cx| {
7300            let settings = *ProjectPanelSettings::get_global(cx);
7301            ProjectPanelSettings::override_global(
7302                ProjectPanelSettings {
7303                    hide_root: true,
7304                    ..settings
7305                },
7306                cx,
7307            );
7308        });
7309
7310        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7311        cx.run_until_parked();
7312
7313        assert_eq!(
7314            visible_entries_as_strings(&panel, 0..10, cx),
7315            &[
7316                "v root1",
7317                "    > dir1",
7318                "    > dir2",
7319                "      file4.txt",
7320                "v root2",
7321                "    > dir3",
7322                "      file6.txt",
7323            ],
7324            "With hide_root=true and multiple worktrees, roots should still be visible"
7325        );
7326    }
7327
7328    // Test 4: Multiple worktrees with hide_root = false
7329    {
7330        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7331        let workspace =
7332            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7333        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7334
7335        cx.update(|_, cx| {
7336            let settings = *ProjectPanelSettings::get_global(cx);
7337            ProjectPanelSettings::override_global(
7338                ProjectPanelSettings {
7339                    hide_root: false,
7340                    ..settings
7341                },
7342                cx,
7343            );
7344        });
7345
7346        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7347        cx.run_until_parked();
7348
7349        assert_eq!(
7350            visible_entries_as_strings(&panel, 0..10, cx),
7351            &[
7352                "v root1",
7353                "    > dir1",
7354                "    > dir2",
7355                "      file4.txt",
7356                "v root2",
7357                "    > dir3",
7358                "      file6.txt",
7359            ],
7360            "With hide_root=false and multiple worktrees, roots should be visible"
7361        );
7362    }
7363}
7364
7365#[gpui::test]
7366async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
7367    init_test_with_editor(cx);
7368
7369    let fs = FakeFs::new(cx.executor());
7370    fs.insert_tree(
7371        "/root",
7372        json!({
7373            "file1.txt": "content of file1",
7374            "file2.txt": "content of file2",
7375            "dir1": {
7376                "file3.txt": "content of file3"
7377            }
7378        }),
7379    )
7380    .await;
7381
7382    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7383    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7384    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7385    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7386    cx.run_until_parked();
7387
7388    let file1_path = "root/file1.txt";
7389    let file2_path = "root/file2.txt";
7390    select_path_with_mark(&panel, file1_path, cx);
7391    select_path_with_mark(&panel, file2_path, cx);
7392
7393    panel.update_in(cx, |panel, window, cx| {
7394        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
7395    });
7396    cx.executor().run_until_parked();
7397
7398    workspace
7399        .update(cx, |workspace, _, cx| {
7400            let active_items = workspace
7401                .panes()
7402                .iter()
7403                .filter_map(|pane| pane.read(cx).active_item())
7404                .collect::<Vec<_>>();
7405            assert_eq!(active_items.len(), 1);
7406            let diff_view = active_items
7407                .into_iter()
7408                .next()
7409                .unwrap()
7410                .downcast::<FileDiffView>()
7411                .expect("Open item should be an FileDiffView");
7412            assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
7413            assert_eq!(
7414                diff_view.tab_tooltip_text(cx).unwrap(),
7415                format!(
7416                    "{}{}",
7417                    rel_path(file1_path).display(PathStyle::local()),
7418                    rel_path(file2_path).display(PathStyle::local())
7419                )
7420            );
7421        })
7422        .unwrap();
7423
7424    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
7425    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
7426    let worktree_id = panel.update(cx, |panel, cx| {
7427        panel
7428            .project
7429            .read(cx)
7430            .worktrees(cx)
7431            .next()
7432            .unwrap()
7433            .read(cx)
7434            .id()
7435    });
7436
7437    let expected_entries = [
7438        SelectedEntry {
7439            worktree_id,
7440            entry_id: file1_entry_id,
7441        },
7442        SelectedEntry {
7443            worktree_id,
7444            entry_id: file2_entry_id,
7445        },
7446    ];
7447    panel.update(cx, |panel, _cx| {
7448        assert_eq!(
7449            &panel.marked_entries, &expected_entries,
7450            "Should keep marked entries after comparison"
7451        );
7452    });
7453
7454    panel.update(cx, |panel, cx| {
7455        panel.project.update(cx, |_, cx| {
7456            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
7457        })
7458    });
7459
7460    panel.update(cx, |panel, _cx| {
7461        assert_eq!(
7462            &panel.marked_entries, &expected_entries,
7463            "Marked entries should persist after focusing back on the project panel"
7464        );
7465    });
7466}
7467
7468#[gpui::test]
7469async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
7470    init_test_with_editor(cx);
7471
7472    let fs = FakeFs::new(cx.executor());
7473    fs.insert_tree(
7474        "/root",
7475        json!({
7476            "file1.txt": "content of file1",
7477            "file2.txt": "content of file2",
7478            "dir1": {},
7479            "dir2": {
7480                "file3.txt": "content of file3"
7481            }
7482        }),
7483    )
7484    .await;
7485
7486    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7487    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7488    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7489    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7490    cx.run_until_parked();
7491
7492    // Test 1: When only one file is selected, there should be no compare option
7493    select_path(&panel, "root/file1.txt", cx);
7494
7495    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7496    assert_eq!(
7497        selected_files, None,
7498        "Should not have compare option when only one file is selected"
7499    );
7500
7501    // Test 2: When multiple files are selected, there should be a compare option
7502    select_path_with_mark(&panel, "root/file1.txt", cx);
7503    select_path_with_mark(&panel, "root/file2.txt", cx);
7504
7505    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7506    assert!(
7507        selected_files.is_some(),
7508        "Should have files selected for comparison"
7509    );
7510    if let Some((file1, file2)) = selected_files {
7511        assert!(
7512            file1.to_string_lossy().ends_with("file1.txt")
7513                && file2.to_string_lossy().ends_with("file2.txt"),
7514            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
7515        );
7516    }
7517
7518    // Test 3: Selecting a directory shouldn't count as a comparable file
7519    select_path_with_mark(&panel, "root/dir1", cx);
7520
7521    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7522    assert!(
7523        selected_files.is_some(),
7524        "Directory selection should not affect comparable files"
7525    );
7526    if let Some((file1, file2)) = selected_files {
7527        assert!(
7528            file1.to_string_lossy().ends_with("file1.txt")
7529                && file2.to_string_lossy().ends_with("file2.txt"),
7530            "Selecting a directory should not affect the number of comparable files"
7531        );
7532    }
7533
7534    // Test 4: Selecting one more file
7535    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
7536
7537    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7538    assert!(
7539        selected_files.is_some(),
7540        "Directory selection should not affect comparable files"
7541    );
7542    if let Some((file1, file2)) = selected_files {
7543        assert!(
7544            file1.to_string_lossy().ends_with("file2.txt")
7545                && file2.to_string_lossy().ends_with("file3.txt"),
7546            "Selecting a directory should not affect the number of comparable files"
7547        );
7548    }
7549}
7550
7551#[gpui::test]
7552async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
7553    init_test(cx);
7554
7555    let fs = FakeFs::new(cx.executor());
7556    fs.insert_tree(
7557        "/root",
7558        json!({
7559            ".hidden-file.txt": "hidden file content",
7560            "visible-file.txt": "visible file content",
7561            ".hidden-parent-dir": {
7562                "nested-dir": {
7563                    "file.txt": "file content",
7564                }
7565            },
7566            "visible-dir": {
7567                "file-in-visible.txt": "file content",
7568                "nested": {
7569                    ".hidden-nested-dir": {
7570                        ".double-hidden-dir": {
7571                            "deep-file-1.txt": "deep content 1",
7572                            "deep-file-2.txt": "deep content 2"
7573                        },
7574                        "hidden-nested-file-1.txt": "hidden nested 1",
7575                        "hidden-nested-file-2.txt": "hidden nested 2"
7576                    },
7577                    "visible-nested-file.txt": "visible nested content"
7578                }
7579            }
7580        }),
7581    )
7582    .await;
7583
7584    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7585    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7586    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7587
7588    cx.update(|_, cx| {
7589        let settings = *ProjectPanelSettings::get_global(cx);
7590        ProjectPanelSettings::override_global(
7591            ProjectPanelSettings {
7592                hide_hidden: false,
7593                ..settings
7594            },
7595            cx,
7596        );
7597    });
7598
7599    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7600    cx.run_until_parked();
7601
7602    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
7603    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
7604    toggle_expand_dir(&panel, "root/visible-dir", cx);
7605    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
7606    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
7607    toggle_expand_dir(
7608        &panel,
7609        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
7610        cx,
7611    );
7612
7613    let expanded = [
7614        "v root",
7615        "    v .hidden-parent-dir",
7616        "        v nested-dir",
7617        "              file.txt",
7618        "    v visible-dir",
7619        "        v nested",
7620        "            v .hidden-nested-dir",
7621        "                v .double-hidden-dir  <== selected",
7622        "                      deep-file-1.txt",
7623        "                      deep-file-2.txt",
7624        "                  hidden-nested-file-1.txt",
7625        "                  hidden-nested-file-2.txt",
7626        "              visible-nested-file.txt",
7627        "          file-in-visible.txt",
7628        "      .hidden-file.txt",
7629        "      visible-file.txt",
7630    ];
7631
7632    assert_eq!(
7633        visible_entries_as_strings(&panel, 0..30, cx),
7634        &expanded,
7635        "With hide_hidden=false, contents of hidden nested directory should be visible"
7636    );
7637
7638    cx.update(|_, cx| {
7639        let settings = *ProjectPanelSettings::get_global(cx);
7640        ProjectPanelSettings::override_global(
7641            ProjectPanelSettings {
7642                hide_hidden: true,
7643                ..settings
7644            },
7645            cx,
7646        );
7647    });
7648
7649    panel.update_in(cx, |panel, window, cx| {
7650        panel.update_visible_entries(None, false, false, window, cx);
7651    });
7652    cx.run_until_parked();
7653
7654    assert_eq!(
7655        visible_entries_as_strings(&panel, 0..30, cx),
7656        &[
7657            "v root",
7658            "    v visible-dir",
7659            "        v nested",
7660            "              visible-nested-file.txt",
7661            "          file-in-visible.txt",
7662            "      visible-file.txt",
7663        ],
7664        "With hide_hidden=false, contents of hidden nested directory should be visible"
7665    );
7666
7667    panel.update_in(cx, |panel, window, cx| {
7668        let settings = *ProjectPanelSettings::get_global(cx);
7669        ProjectPanelSettings::override_global(
7670            ProjectPanelSettings {
7671                hide_hidden: false,
7672                ..settings
7673            },
7674            cx,
7675        );
7676        panel.update_visible_entries(None, false, false, window, cx);
7677    });
7678    cx.run_until_parked();
7679
7680    assert_eq!(
7681        visible_entries_as_strings(&panel, 0..30, cx),
7682        &expanded,
7683        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
7684    );
7685}
7686
7687fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7688    let path = rel_path(path);
7689    panel.update_in(cx, |panel, window, cx| {
7690        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7691            let worktree = worktree.read(cx);
7692            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7693                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7694                panel.update_visible_entries(
7695                    Some((worktree.id(), entry_id)),
7696                    false,
7697                    false,
7698                    window,
7699                    cx,
7700                );
7701                return;
7702            }
7703        }
7704        panic!("no worktree for path {:?}", path);
7705    });
7706    cx.run_until_parked();
7707}
7708
7709fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7710    let path = rel_path(path);
7711    panel.update(cx, |panel, cx| {
7712        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7713            let worktree = worktree.read(cx);
7714            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7715                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7716                let entry = crate::SelectedEntry {
7717                    worktree_id: worktree.id(),
7718                    entry_id,
7719                };
7720                if !panel.marked_entries.contains(&entry) {
7721                    panel.marked_entries.push(entry);
7722                }
7723                panel.state.selection = Some(entry);
7724                return;
7725            }
7726        }
7727        panic!("no worktree for path {:?}", path);
7728    });
7729}
7730
7731fn drag_selection_to(
7732    panel: &Entity<ProjectPanel>,
7733    target_path: &str,
7734    is_file: bool,
7735    cx: &mut VisualTestContext,
7736) {
7737    let target_entry = find_project_entry(panel, target_path, cx)
7738        .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
7739
7740    panel.update_in(cx, |panel, window, cx| {
7741        let selection = panel
7742            .state
7743            .selection
7744            .expect("a selection is required before dragging");
7745        let drag = DraggedSelection {
7746            active_selection: SelectedEntry {
7747                worktree_id: selection.worktree_id,
7748                entry_id: panel.resolve_entry(selection.entry_id),
7749            },
7750            marked_selections: Arc::from(panel.marked_entries.clone()),
7751        };
7752        panel.drag_onto(&drag, target_entry, is_file, window, cx);
7753    });
7754    cx.executor().run_until_parked();
7755}
7756
7757fn find_project_entry(
7758    panel: &Entity<ProjectPanel>,
7759    path: &str,
7760    cx: &mut VisualTestContext,
7761) -> Option<ProjectEntryId> {
7762    let path = rel_path(path);
7763    panel.update(cx, |panel, cx| {
7764        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7765            let worktree = worktree.read(cx);
7766            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7767                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
7768            }
7769        }
7770        panic!("no worktree for path {path:?}");
7771    })
7772}
7773
7774fn visible_entries_as_strings(
7775    panel: &Entity<ProjectPanel>,
7776    range: Range<usize>,
7777    cx: &mut VisualTestContext,
7778) -> Vec<String> {
7779    let mut result = Vec::new();
7780    let mut project_entries = HashSet::default();
7781    let mut has_editor = false;
7782
7783    panel.update_in(cx, |panel, window, cx| {
7784        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
7785            if details.is_editing {
7786                assert!(!has_editor, "duplicate editor entry");
7787                has_editor = true;
7788            } else {
7789                assert!(
7790                    project_entries.insert(project_entry),
7791                    "duplicate project entry {:?} {:?}",
7792                    project_entry,
7793                    details
7794                );
7795            }
7796
7797            let indent = "    ".repeat(details.depth);
7798            let icon = if details.kind.is_dir() {
7799                if details.is_expanded { "v " } else { "> " }
7800            } else {
7801                "  "
7802            };
7803            #[cfg(windows)]
7804            let filename = details.filename.replace("\\", "/");
7805            #[cfg(not(windows))]
7806            let filename = details.filename;
7807            let name = if details.is_editing {
7808                format!("[EDITOR: '{}']", filename)
7809            } else if details.is_processing {
7810                format!("[PROCESSING: '{}']", filename)
7811            } else {
7812                filename
7813            };
7814            let selected = if details.is_selected {
7815                "  <== selected"
7816            } else {
7817                ""
7818            };
7819            let marked = if details.is_marked {
7820                "  <== marked"
7821            } else {
7822                ""
7823            };
7824
7825            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
7826        });
7827    });
7828
7829    result
7830}
7831
7832/// Test that missing sort_mode field defaults to DirectoriesFirst
7833#[gpui::test]
7834async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
7835    init_test(cx);
7836
7837    // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
7838    let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
7839    assert_eq!(
7840        default_settings.sort_mode,
7841        settings::ProjectPanelSortMode::DirectoriesFirst,
7842        "sort_mode should default to DirectoriesFirst"
7843    );
7844}
7845
7846/// Test sort modes: DirectoriesFirst (default) vs Mixed
7847#[gpui::test]
7848async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
7849    init_test(cx);
7850
7851    let fs = FakeFs::new(cx.executor());
7852    fs.insert_tree(
7853        "/root",
7854        json!({
7855            "zebra.txt": "",
7856            "Apple": {},
7857            "banana.rs": "",
7858            "Carrot": {},
7859            "aardvark.txt": "",
7860        }),
7861    )
7862    .await;
7863
7864    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7865    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7866    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7867    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7868    cx.run_until_parked();
7869
7870    // Default sort mode should be DirectoriesFirst
7871    assert_eq!(
7872        visible_entries_as_strings(&panel, 0..50, cx),
7873        &[
7874            "v root",
7875            "    > Apple",
7876            "    > Carrot",
7877            "      aardvark.txt",
7878            "      banana.rs",
7879            "      zebra.txt",
7880        ]
7881    );
7882}
7883
7884#[gpui::test]
7885async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
7886    init_test(cx);
7887
7888    let fs = FakeFs::new(cx.executor());
7889    fs.insert_tree(
7890        "/root",
7891        json!({
7892            "Zebra.txt": "",
7893            "apple": {},
7894            "Banana.rs": "",
7895            "carrot": {},
7896            "Aardvark.txt": "",
7897        }),
7898    )
7899    .await;
7900
7901    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7902    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7903    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7904
7905    // Switch to Mixed mode
7906    cx.update(|_, cx| {
7907        cx.update_global::<SettingsStore, _>(|store, cx| {
7908            store.update_user_settings(cx, |settings| {
7909                settings.project_panel.get_or_insert_default().sort_mode =
7910                    Some(settings::ProjectPanelSortMode::Mixed);
7911            });
7912        });
7913    });
7914
7915    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7916    cx.run_until_parked();
7917
7918    // Mixed mode: case-insensitive sorting
7919    // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
7920    assert_eq!(
7921        visible_entries_as_strings(&panel, 0..50, cx),
7922        &[
7923            "v root",
7924            "      Aardvark.txt",
7925            "    > apple",
7926            "      Banana.rs",
7927            "    > carrot",
7928            "      Zebra.txt",
7929        ]
7930    );
7931}
7932
7933#[gpui::test]
7934async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
7935    init_test(cx);
7936
7937    let fs = FakeFs::new(cx.executor());
7938    fs.insert_tree(
7939        "/root",
7940        json!({
7941            "Zebra.txt": "",
7942            "apple": {},
7943            "Banana.rs": "",
7944            "carrot": {},
7945            "Aardvark.txt": "",
7946        }),
7947    )
7948    .await;
7949
7950    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7951    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7952    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7953
7954    // Switch to FilesFirst mode
7955    cx.update(|_, cx| {
7956        cx.update_global::<SettingsStore, _>(|store, cx| {
7957            store.update_user_settings(cx, |settings| {
7958                settings.project_panel.get_or_insert_default().sort_mode =
7959                    Some(settings::ProjectPanelSortMode::FilesFirst);
7960            });
7961        });
7962    });
7963
7964    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7965    cx.run_until_parked();
7966
7967    // FilesFirst mode: files first, then directories (both case-insensitive)
7968    assert_eq!(
7969        visible_entries_as_strings(&panel, 0..50, cx),
7970        &[
7971            "v root",
7972            "      Aardvark.txt",
7973            "      Banana.rs",
7974            "      Zebra.txt",
7975            "    > apple",
7976            "    > carrot",
7977        ]
7978    );
7979}
7980
7981#[gpui::test]
7982async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
7983    init_test(cx);
7984
7985    let fs = FakeFs::new(cx.executor());
7986    fs.insert_tree(
7987        "/root",
7988        json!({
7989            "file2.txt": "",
7990            "dir1": {},
7991            "file1.txt": "",
7992        }),
7993    )
7994    .await;
7995
7996    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7997    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7998    let cx = &mut VisualTestContext::from_window(*workspace, cx);
7999    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
8000    cx.run_until_parked();
8001
8002    // Initially DirectoriesFirst
8003    assert_eq!(
8004        visible_entries_as_strings(&panel, 0..50, cx),
8005        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
8006    );
8007
8008    // Toggle to Mixed
8009    cx.update(|_, cx| {
8010        cx.update_global::<SettingsStore, _>(|store, cx| {
8011            store.update_user_settings(cx, |settings| {
8012                settings.project_panel.get_or_insert_default().sort_mode =
8013                    Some(settings::ProjectPanelSortMode::Mixed);
8014            });
8015        });
8016    });
8017    cx.run_until_parked();
8018
8019    assert_eq!(
8020        visible_entries_as_strings(&panel, 0..50, cx),
8021        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
8022    );
8023
8024    // Toggle back to DirectoriesFirst
8025    cx.update(|_, cx| {
8026        cx.update_global::<SettingsStore, _>(|store, cx| {
8027            store.update_user_settings(cx, |settings| {
8028                settings.project_panel.get_or_insert_default().sort_mode =
8029                    Some(settings::ProjectPanelSortMode::DirectoriesFirst);
8030            });
8031        });
8032    });
8033    cx.run_until_parked();
8034
8035    assert_eq!(
8036        visible_entries_as_strings(&panel, 0..50, cx),
8037        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
8038    );
8039}
8040
8041fn init_test(cx: &mut TestAppContext) {
8042    cx.update(|cx| {
8043        let settings_store = SettingsStore::test(cx);
8044        cx.set_global(settings_store);
8045        theme::init(theme::LoadThemes::JustBase, cx);
8046        crate::init(cx);
8047
8048        cx.update_global::<SettingsStore, _>(|store, cx| {
8049            store.update_user_settings(cx, |settings| {
8050                settings
8051                    .project_panel
8052                    .get_or_insert_default()
8053                    .auto_fold_dirs = Some(false);
8054                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
8055            });
8056        });
8057    });
8058}
8059
8060fn init_test_with_editor(cx: &mut TestAppContext) {
8061    cx.update(|cx| {
8062        let app_state = AppState::test(cx);
8063        theme::init(theme::LoadThemes::JustBase, cx);
8064        editor::init(cx);
8065        crate::init(cx);
8066        workspace::init(app_state, cx);
8067
8068        cx.update_global::<SettingsStore, _>(|store, cx| {
8069            store.update_user_settings(cx, |settings| {
8070                settings
8071                    .project_panel
8072                    .get_or_insert_default()
8073                    .auto_fold_dirs = Some(false);
8074                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
8075            });
8076        });
8077    });
8078}
8079
8080fn set_auto_open_settings(
8081    cx: &mut TestAppContext,
8082    auto_open_settings: ProjectPanelAutoOpenSettings,
8083) {
8084    cx.update(|cx| {
8085        cx.update_global::<SettingsStore, _>(|store, cx| {
8086            store.update_user_settings(cx, |settings| {
8087                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
8088            });
8089        })
8090    });
8091}
8092
8093fn ensure_single_file_is_opened(
8094    window: &WindowHandle<Workspace>,
8095    expected_path: &str,
8096    cx: &mut TestAppContext,
8097) {
8098    window
8099        .update(cx, |workspace, _, cx| {
8100            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
8101            assert_eq!(worktrees.len(), 1);
8102            let worktree_id = worktrees[0].read(cx).id();
8103
8104            let open_project_paths = workspace
8105                .panes()
8106                .iter()
8107                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
8108                .collect::<Vec<_>>();
8109            assert_eq!(
8110                open_project_paths,
8111                vec![ProjectPath {
8112                    worktree_id,
8113                    path: Arc::from(rel_path(expected_path))
8114                }],
8115                "Should have opened file, selected in project panel"
8116            );
8117        })
8118        .unwrap();
8119}
8120
8121fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8122    assert!(
8123        !cx.has_pending_prompt(),
8124        "Should have no prompts before the deletion"
8125    );
8126    panel.update_in(cx, |panel, window, cx| {
8127        panel.delete(&Delete { skip_prompt: false }, window, cx)
8128    });
8129    assert!(
8130        cx.has_pending_prompt(),
8131        "Should have a prompt after the deletion"
8132    );
8133    cx.simulate_prompt_answer("Delete");
8134    assert!(
8135        !cx.has_pending_prompt(),
8136        "Should have no prompts after prompt was replied to"
8137    );
8138    cx.executor().run_until_parked();
8139}
8140
8141fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8142    assert!(
8143        !cx.has_pending_prompt(),
8144        "Should have no prompts before the deletion"
8145    );
8146    panel.update_in(cx, |panel, window, cx| {
8147        panel.delete(&Delete { skip_prompt: true }, window, cx)
8148    });
8149    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
8150    cx.executor().run_until_parked();
8151}
8152
8153fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
8154    assert!(
8155        !cx.has_pending_prompt(),
8156        "Should have no prompts after deletion operation closes the file"
8157    );
8158    workspace
8159        .read_with(cx, |workspace, cx| {
8160            let open_project_paths = workspace
8161                .panes()
8162                .iter()
8163                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
8164                .collect::<Vec<_>>();
8165            assert!(
8166                open_project_paths.is_empty(),
8167                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
8168            );
8169        })
8170        .unwrap();
8171}
8172
8173struct TestProjectItemView {
8174    focus_handle: FocusHandle,
8175    path: ProjectPath,
8176}
8177
8178struct TestProjectItem {
8179    path: ProjectPath,
8180}
8181
8182impl project::ProjectItem for TestProjectItem {
8183    fn try_open(
8184        _project: &Entity<Project>,
8185        path: &ProjectPath,
8186        cx: &mut App,
8187    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
8188        let path = path.clone();
8189        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
8190    }
8191
8192    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8193        None
8194    }
8195
8196    fn project_path(&self, _: &App) -> Option<ProjectPath> {
8197        Some(self.path.clone())
8198    }
8199
8200    fn is_dirty(&self) -> bool {
8201        false
8202    }
8203}
8204
8205impl ProjectItem for TestProjectItemView {
8206    type Item = TestProjectItem;
8207
8208    fn for_project_item(
8209        _: Entity<Project>,
8210        _: Option<&Pane>,
8211        project_item: Entity<Self::Item>,
8212        _: &mut Window,
8213        cx: &mut Context<Self>,
8214    ) -> Self
8215    where
8216        Self: Sized,
8217    {
8218        Self {
8219            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
8220            focus_handle: cx.focus_handle(),
8221        }
8222    }
8223}
8224
8225impl Item for TestProjectItemView {
8226    type Event = ();
8227
8228    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
8229        "Test".into()
8230    }
8231}
8232
8233impl EventEmitter<()> for TestProjectItemView {}
8234
8235impl Focusable for TestProjectItemView {
8236    fn focus_handle(&self, _: &App) -> FocusHandle {
8237        self.focus_handle.clone()
8238    }
8239}
8240
8241impl Render for TestProjectItemView {
8242    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
8243        Empty
8244    }
8245}