project_panel_tests.rs

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