project_panel_tests.rs

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