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