project_panel_tests.rs

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