project_panel_tests.rs

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