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