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