project_panel_tests.rs

   1use super::*;
   2use collections::HashSet;
   3use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
   4use pretty_assertions::assert_eq;
   5use project::{FakeFs, WorktreeSettings};
   6use serde_json::json;
   7use settings::SettingsStore;
   8use std::path::{Path, PathBuf};
   9use util::{path, separator};
  10use workspace::{
  11    AppState, Pane,
  12    item::{Item, ProjectItem},
  13    register_project_item,
  14};
  15
  16#[gpui::test]
  17async fn test_visible_list(cx: &mut gpui::TestAppContext) {
  18    init_test(cx);
  19
  20    let fs = FakeFs::new(cx.executor().clone());
  21    fs.insert_tree(
  22        "/root1",
  23        json!({
  24            ".dockerignore": "",
  25            ".git": {
  26                "HEAD": "",
  27            },
  28            "a": {
  29                "0": { "q": "", "r": "", "s": "" },
  30                "1": { "t": "", "u": "" },
  31                "2": { "v": "", "w": "", "x": "", "y": "" },
  32            },
  33            "b": {
  34                "3": { "Q": "" },
  35                "4": { "R": "", "S": "", "T": "", "U": "" },
  36            },
  37            "C": {
  38                "5": {},
  39                "6": { "V": "", "W": "" },
  40                "7": { "X": "" },
  41                "8": { "Y": {}, "Z": "" }
  42            }
  43        }),
  44    )
  45    .await;
  46    fs.insert_tree(
  47        "/root2",
  48        json!({
  49            "d": {
  50                "9": ""
  51            },
  52            "e": {}
  53        }),
  54    )
  55    .await;
  56
  57    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  58    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
  59    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  60    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
  61    assert_eq!(
  62        visible_entries_as_strings(&panel, 0..50, cx),
  63        &[
  64            "v root1",
  65            "    > .git",
  66            "    > a",
  67            "    > b",
  68            "    > C",
  69            "      .dockerignore",
  70            "v root2",
  71            "    > d",
  72            "    > e",
  73        ]
  74    );
  75
  76    toggle_expand_dir(&panel, "root1/b", cx);
  77    assert_eq!(
  78        visible_entries_as_strings(&panel, 0..50, cx),
  79        &[
  80            "v root1",
  81            "    > .git",
  82            "    > a",
  83            "    v b  <== selected",
  84            "        > 3",
  85            "        > 4",
  86            "    > C",
  87            "      .dockerignore",
  88            "v root2",
  89            "    > d",
  90            "    > e",
  91        ]
  92    );
  93
  94    assert_eq!(
  95        visible_entries_as_strings(&panel, 6..9, cx),
  96        &[
  97            //
  98            "    > C",
  99            "      .dockerignore",
 100            "v root2",
 101        ]
 102    );
 103}
 104
 105#[gpui::test]
 106async fn test_opening_file(cx: &mut gpui::TestAppContext) {
 107    init_test_with_editor(cx);
 108
 109    let fs = FakeFs::new(cx.executor().clone());
 110    fs.insert_tree(
 111        path!("/src"),
 112        json!({
 113            "test": {
 114                "first.rs": "// First Rust file",
 115                "second.rs": "// Second Rust file",
 116                "third.rs": "// Third Rust file",
 117            }
 118        }),
 119    )
 120    .await;
 121
 122    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
 123    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 124    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 125    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 126
 127    toggle_expand_dir(&panel, "src/test", cx);
 128    select_path(&panel, "src/test/first.rs", cx);
 129    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 130    cx.executor().run_until_parked();
 131    assert_eq!(
 132        visible_entries_as_strings(&panel, 0..10, cx),
 133        &[
 134            "v src",
 135            "    v test",
 136            "          first.rs  <== selected  <== marked",
 137            "          second.rs",
 138            "          third.rs"
 139        ]
 140    );
 141    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
 142
 143    select_path(&panel, "src/test/second.rs", cx);
 144    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 145    cx.executor().run_until_parked();
 146    assert_eq!(
 147        visible_entries_as_strings(&panel, 0..10, cx),
 148        &[
 149            "v src",
 150            "    v test",
 151            "          first.rs",
 152            "          second.rs  <== selected  <== marked",
 153            "          third.rs"
 154        ]
 155    );
 156    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
 157}
 158
 159#[gpui::test]
 160async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
 161    init_test(cx);
 162    cx.update(|cx| {
 163        cx.update_global::<SettingsStore, _>(|store, cx| {
 164            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
 165                worktree_settings.file_scan_exclusions =
 166                    Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
 167            });
 168        });
 169    });
 170
 171    let fs = FakeFs::new(cx.background_executor.clone());
 172    fs.insert_tree(
 173        "/root1",
 174        json!({
 175            ".dockerignore": "",
 176            ".git": {
 177                "HEAD": "",
 178            },
 179            "a": {
 180                "0": { "q": "", "r": "", "s": "" },
 181                "1": { "t": "", "u": "" },
 182                "2": { "v": "", "w": "", "x": "", "y": "" },
 183            },
 184            "b": {
 185                "3": { "Q": "" },
 186                "4": { "R": "", "S": "", "T": "", "U": "" },
 187            },
 188            "C": {
 189                "5": {},
 190                "6": { "V": "", "W": "" },
 191                "7": { "X": "" },
 192                "8": { "Y": {}, "Z": "" }
 193            }
 194        }),
 195    )
 196    .await;
 197    fs.insert_tree(
 198        "/root2",
 199        json!({
 200            "d": {
 201                "4": ""
 202            },
 203            "e": {}
 204        }),
 205    )
 206    .await;
 207
 208    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 209    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 210    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 211    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 212    assert_eq!(
 213        visible_entries_as_strings(&panel, 0..50, cx),
 214        &[
 215            "v root1",
 216            "    > a",
 217            "    > b",
 218            "    > C",
 219            "      .dockerignore",
 220            "v root2",
 221            "    > d",
 222            "    > e",
 223        ]
 224    );
 225
 226    toggle_expand_dir(&panel, "root1/b", cx);
 227    assert_eq!(
 228        visible_entries_as_strings(&panel, 0..50, cx),
 229        &[
 230            "v root1",
 231            "    > a",
 232            "    v b  <== selected",
 233            "        > 3",
 234            "    > C",
 235            "      .dockerignore",
 236            "v root2",
 237            "    > d",
 238            "    > e",
 239        ]
 240    );
 241
 242    toggle_expand_dir(&panel, "root2/d", cx);
 243    assert_eq!(
 244        visible_entries_as_strings(&panel, 0..50, cx),
 245        &[
 246            "v root1",
 247            "    > a",
 248            "    v b",
 249            "        > 3",
 250            "    > C",
 251            "      .dockerignore",
 252            "v root2",
 253            "    v d  <== selected",
 254            "    > e",
 255        ]
 256    );
 257
 258    toggle_expand_dir(&panel, "root2/e", cx);
 259    assert_eq!(
 260        visible_entries_as_strings(&panel, 0..50, cx),
 261        &[
 262            "v root1",
 263            "    > a",
 264            "    v b",
 265            "        > 3",
 266            "    > C",
 267            "      .dockerignore",
 268            "v root2",
 269            "    v d",
 270            "    v e  <== selected",
 271        ]
 272    );
 273}
 274
 275#[gpui::test]
 276async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
 277    init_test(cx);
 278
 279    let fs = FakeFs::new(cx.executor().clone());
 280    fs.insert_tree(
 281        path!("/root1"),
 282        json!({
 283            "dir_1": {
 284                "nested_dir_1": {
 285                    "nested_dir_2": {
 286                        "nested_dir_3": {
 287                            "file_a.java": "// File contents",
 288                            "file_b.java": "// File contents",
 289                            "file_c.java": "// File contents",
 290                            "nested_dir_4": {
 291                                "nested_dir_5": {
 292                                    "file_d.java": "// File contents",
 293                                }
 294                            }
 295                        }
 296                    }
 297                }
 298            }
 299        }),
 300    )
 301    .await;
 302    fs.insert_tree(
 303        path!("/root2"),
 304        json!({
 305            "dir_2": {
 306                "file_1.java": "// File contents",
 307            }
 308        }),
 309    )
 310    .await;
 311
 312    let project = Project::test(
 313        fs.clone(),
 314        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 315        cx,
 316    )
 317    .await;
 318    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 319    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 320    cx.update(|_, cx| {
 321        let settings = *ProjectPanelSettings::get_global(cx);
 322        ProjectPanelSettings::override_global(
 323            ProjectPanelSettings {
 324                auto_fold_dirs: true,
 325                ..settings
 326            },
 327            cx,
 328        );
 329    });
 330    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
 331    assert_eq!(
 332        visible_entries_as_strings(&panel, 0..10, cx),
 333        &[
 334            separator!("v root1"),
 335            separator!("    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
 336            separator!("v root2"),
 337            separator!("    > dir_2"),
 338        ]
 339    );
 340
 341    toggle_expand_dir(
 342        &panel,
 343        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
 344        cx,
 345    );
 346    assert_eq!(
 347        visible_entries_as_strings(&panel, 0..10, cx),
 348        &[
 349            separator!("v root1"),
 350            separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected"),
 351            separator!("        > nested_dir_4/nested_dir_5"),
 352            separator!("          file_a.java"),
 353            separator!("          file_b.java"),
 354            separator!("          file_c.java"),
 355            separator!("v root2"),
 356            separator!("    > dir_2"),
 357        ]
 358    );
 359
 360    toggle_expand_dir(
 361        &panel,
 362        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
 363        cx,
 364    );
 365    assert_eq!(
 366        visible_entries_as_strings(&panel, 0..10, cx),
 367        &[
 368            separator!("v root1"),
 369            separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
 370            separator!("        v nested_dir_4/nested_dir_5  <== selected"),
 371            separator!("              file_d.java"),
 372            separator!("          file_a.java"),
 373            separator!("          file_b.java"),
 374            separator!("          file_c.java"),
 375            separator!("v root2"),
 376            separator!("    > dir_2"),
 377        ]
 378    );
 379    toggle_expand_dir(&panel, "root2/dir_2", cx);
 380    assert_eq!(
 381        visible_entries_as_strings(&panel, 0..10, cx),
 382        &[
 383            separator!("v root1"),
 384            separator!("    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
 385            separator!("        v nested_dir_4/nested_dir_5"),
 386            separator!("              file_d.java"),
 387            separator!("          file_a.java"),
 388            separator!("          file_b.java"),
 389            separator!("          file_c.java"),
 390            separator!("v root2"),
 391            separator!("    v dir_2  <== selected"),
 392            separator!("          file_1.java"),
 393        ]
 394    );
 395}
 396
 397#[gpui::test(iterations = 30)]
 398async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 399    init_test(cx);
 400
 401    let fs = FakeFs::new(cx.executor().clone());
 402    fs.insert_tree(
 403        "/root1",
 404        json!({
 405            ".dockerignore": "",
 406            ".git": {
 407                "HEAD": "",
 408            },
 409            "a": {
 410                "0": { "q": "", "r": "", "s": "" },
 411                "1": { "t": "", "u": "" },
 412                "2": { "v": "", "w": "", "x": "", "y": "" },
 413            },
 414            "b": {
 415                "3": { "Q": "" },
 416                "4": { "R": "", "S": "", "T": "", "U": "" },
 417            },
 418            "C": {
 419                "5": {},
 420                "6": { "V": "", "W": "" },
 421                "7": { "X": "" },
 422                "8": { "Y": {}, "Z": "" }
 423            }
 424        }),
 425    )
 426    .await;
 427    fs.insert_tree(
 428        "/root2",
 429        json!({
 430            "d": {
 431                "9": ""
 432            },
 433            "e": {}
 434        }),
 435    )
 436    .await;
 437
 438    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 439    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 440    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 441    let panel = workspace
 442        .update(cx, |workspace, window, cx| {
 443            let panel = ProjectPanel::new(workspace, window, cx);
 444            workspace.add_panel(panel.clone(), window, cx);
 445            panel
 446        })
 447        .unwrap();
 448
 449    select_path(&panel, "root1", cx);
 450    assert_eq!(
 451        visible_entries_as_strings(&panel, 0..10, cx),
 452        &[
 453            "v root1  <== selected",
 454            "    > .git",
 455            "    > a",
 456            "    > b",
 457            "    > C",
 458            "      .dockerignore",
 459            "v root2",
 460            "    > d",
 461            "    > e",
 462        ]
 463    );
 464
 465    // Add a file with the root folder selected. The filename editor is placed
 466    // before the first file in the root folder.
 467    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 468    panel.update_in(cx, |panel, window, cx| {
 469        assert!(panel.filename_editor.read(cx).is_focused(window));
 470    });
 471    assert_eq!(
 472        visible_entries_as_strings(&panel, 0..10, cx),
 473        &[
 474            "v root1",
 475            "    > .git",
 476            "    > a",
 477            "    > b",
 478            "    > C",
 479            "      [EDITOR: '']  <== selected",
 480            "      .dockerignore",
 481            "v root2",
 482            "    > d",
 483            "    > e",
 484        ]
 485    );
 486
 487    let confirm = panel.update_in(cx, |panel, window, cx| {
 488        panel.filename_editor.update(cx, |editor, cx| {
 489            editor.set_text("the-new-filename", window, cx)
 490        });
 491        panel.confirm_edit(window, cx).unwrap()
 492    });
 493    assert_eq!(
 494        visible_entries_as_strings(&panel, 0..10, cx),
 495        &[
 496            "v root1",
 497            "    > .git",
 498            "    > a",
 499            "    > b",
 500            "    > C",
 501            "      [PROCESSING: 'the-new-filename']  <== selected",
 502            "      .dockerignore",
 503            "v root2",
 504            "    > d",
 505            "    > e",
 506        ]
 507    );
 508
 509    confirm.await.unwrap();
 510    assert_eq!(
 511        visible_entries_as_strings(&panel, 0..10, cx),
 512        &[
 513            "v root1",
 514            "    > .git",
 515            "    > a",
 516            "    > b",
 517            "    > C",
 518            "      .dockerignore",
 519            "      the-new-filename  <== selected  <== marked",
 520            "v root2",
 521            "    > d",
 522            "    > e",
 523        ]
 524    );
 525
 526    select_path(&panel, "root1/b", cx);
 527    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 528    assert_eq!(
 529        visible_entries_as_strings(&panel, 0..10, cx),
 530        &[
 531            "v root1",
 532            "    > .git",
 533            "    > a",
 534            "    v b",
 535            "        > 3",
 536            "        > 4",
 537            "          [EDITOR: '']  <== selected",
 538            "    > C",
 539            "      .dockerignore",
 540            "      the-new-filename",
 541        ]
 542    );
 543
 544    panel
 545        .update_in(cx, |panel, window, cx| {
 546            panel.filename_editor.update(cx, |editor, cx| {
 547                editor.set_text("another-filename.txt", window, cx)
 548            });
 549            panel.confirm_edit(window, cx).unwrap()
 550        })
 551        .await
 552        .unwrap();
 553    assert_eq!(
 554        visible_entries_as_strings(&panel, 0..10, cx),
 555        &[
 556            "v root1",
 557            "    > .git",
 558            "    > a",
 559            "    v b",
 560            "        > 3",
 561            "        > 4",
 562            "          another-filename.txt  <== selected  <== marked",
 563            "    > C",
 564            "      .dockerignore",
 565            "      the-new-filename",
 566        ]
 567    );
 568
 569    select_path(&panel, "root1/b/another-filename.txt", cx);
 570    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 571    assert_eq!(
 572        visible_entries_as_strings(&panel, 0..10, cx),
 573        &[
 574            "v root1",
 575            "    > .git",
 576            "    > a",
 577            "    v b",
 578            "        > 3",
 579            "        > 4",
 580            "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
 581            "    > C",
 582            "      .dockerignore",
 583            "      the-new-filename",
 584        ]
 585    );
 586
 587    let confirm = panel.update_in(cx, |panel, window, cx| {
 588        panel.filename_editor.update(cx, |editor, cx| {
 589            let file_name_selections = editor.selections.all::<usize>(cx);
 590            assert_eq!(
 591                file_name_selections.len(),
 592                1,
 593                "File editing should have a single selection, but got: {file_name_selections:?}"
 594            );
 595            let file_name_selection = &file_name_selections[0];
 596            assert_eq!(
 597                file_name_selection.start, 0,
 598                "Should select the file name from the start"
 599            );
 600            assert_eq!(
 601                file_name_selection.end,
 602                "another-filename".len(),
 603                "Should not select file extension"
 604            );
 605
 606            editor.set_text("a-different-filename.tar.gz", window, cx)
 607        });
 608        panel.confirm_edit(window, cx).unwrap()
 609    });
 610    assert_eq!(
 611        visible_entries_as_strings(&panel, 0..10, cx),
 612        &[
 613            "v root1",
 614            "    > .git",
 615            "    > a",
 616            "    v b",
 617            "        > 3",
 618            "        > 4",
 619            "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
 620            "    > C",
 621            "      .dockerignore",
 622            "      the-new-filename",
 623        ]
 624    );
 625
 626    confirm.await.unwrap();
 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            "          a-different-filename.tar.gz  <== selected",
 637            "    > C",
 638            "      .dockerignore",
 639            "      the-new-filename",
 640        ]
 641    );
 642
 643    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 644    assert_eq!(
 645        visible_entries_as_strings(&panel, 0..10, cx),
 646        &[
 647            "v root1",
 648            "    > .git",
 649            "    > a",
 650            "    v b",
 651            "        > 3",
 652            "        > 4",
 653            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
 654            "    > C",
 655            "      .dockerignore",
 656            "      the-new-filename",
 657        ]
 658    );
 659
 660    panel.update_in(cx, |panel, window, cx| {
 661            panel.filename_editor.update(cx, |editor, cx| {
 662                let file_name_selections = editor.selections.all::<usize>(cx);
 663                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
 664                let file_name_selection = &file_name_selections[0];
 665                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
 666                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
 667
 668            });
 669            panel.cancel(&menu::Cancel, window, cx)
 670        });
 671
 672    panel.update_in(cx, |panel, window, cx| {
 673        panel.new_directory(&NewDirectory, window, cx)
 674    });
 675    assert_eq!(
 676        visible_entries_as_strings(&panel, 0..10, cx),
 677        &[
 678            "v root1",
 679            "    > .git",
 680            "    > a",
 681            "    v b",
 682            "        > 3",
 683            "        > 4",
 684            "        > [EDITOR: '']  <== selected",
 685            "          a-different-filename.tar.gz",
 686            "    > C",
 687            "      .dockerignore",
 688        ]
 689    );
 690
 691    let confirm = panel.update_in(cx, |panel, window, cx| {
 692        panel
 693            .filename_editor
 694            .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
 695        panel.confirm_edit(window, cx).unwrap()
 696    });
 697    panel.update_in(cx, |panel, window, cx| {
 698        panel.select_next(&Default::default(), window, cx)
 699    });
 700    assert_eq!(
 701        visible_entries_as_strings(&panel, 0..10, cx),
 702        &[
 703            "v root1",
 704            "    > .git",
 705            "    > a",
 706            "    v b",
 707            "        > 3",
 708            "        > 4",
 709            "        > [PROCESSING: 'new-dir']",
 710            "          a-different-filename.tar.gz  <== selected",
 711            "    > C",
 712            "      .dockerignore",
 713        ]
 714    );
 715
 716    confirm.await.unwrap();
 717    assert_eq!(
 718        visible_entries_as_strings(&panel, 0..10, cx),
 719        &[
 720            "v root1",
 721            "    > .git",
 722            "    > a",
 723            "    v b",
 724            "        > 3",
 725            "        > 4",
 726            "        > new-dir",
 727            "          a-different-filename.tar.gz  <== selected",
 728            "    > C",
 729            "      .dockerignore",
 730        ]
 731    );
 732
 733    panel.update_in(cx, |panel, window, cx| {
 734        panel.rename(&Default::default(), window, cx)
 735    });
 736    assert_eq!(
 737        visible_entries_as_strings(&panel, 0..10, cx),
 738        &[
 739            "v root1",
 740            "    > .git",
 741            "    > a",
 742            "    v b",
 743            "        > 3",
 744            "        > 4",
 745            "        > new-dir",
 746            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
 747            "    > C",
 748            "      .dockerignore",
 749        ]
 750    );
 751
 752    // Dismiss the rename editor when it loses focus.
 753    workspace.update(cx, |_, window, _| window.blur()).unwrap();
 754    assert_eq!(
 755        visible_entries_as_strings(&panel, 0..10, cx),
 756        &[
 757            "v root1",
 758            "    > .git",
 759            "    > a",
 760            "    v b",
 761            "        > 3",
 762            "        > 4",
 763            "        > new-dir",
 764            "          a-different-filename.tar.gz  <== selected",
 765            "    > C",
 766            "      .dockerignore",
 767        ]
 768    );
 769}
 770
 771#[gpui::test(iterations = 10)]
 772async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
 773    init_test(cx);
 774
 775    let fs = FakeFs::new(cx.executor().clone());
 776    fs.insert_tree(
 777        "/root1",
 778        json!({
 779            ".dockerignore": "",
 780            ".git": {
 781                "HEAD": "",
 782            },
 783            "a": {
 784                "0": { "q": "", "r": "", "s": "" },
 785                "1": { "t": "", "u": "" },
 786                "2": { "v": "", "w": "", "x": "", "y": "" },
 787            },
 788            "b": {
 789                "3": { "Q": "" },
 790                "4": { "R": "", "S": "", "T": "", "U": "" },
 791            },
 792            "C": {
 793                "5": {},
 794                "6": { "V": "", "W": "" },
 795                "7": { "X": "" },
 796                "8": { "Y": {}, "Z": "" }
 797            }
 798        }),
 799    )
 800    .await;
 801    fs.insert_tree(
 802        "/root2",
 803        json!({
 804            "d": {
 805                "9": ""
 806            },
 807            "e": {}
 808        }),
 809    )
 810    .await;
 811
 812    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 813    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 814    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 815    let panel = workspace
 816        .update(cx, |workspace, window, cx| {
 817            let panel = ProjectPanel::new(workspace, window, cx);
 818            workspace.add_panel(panel.clone(), window, cx);
 819            panel
 820        })
 821        .unwrap();
 822
 823    select_path(&panel, "root1", cx);
 824    assert_eq!(
 825        visible_entries_as_strings(&panel, 0..10, cx),
 826        &[
 827            "v root1  <== selected",
 828            "    > .git",
 829            "    > a",
 830            "    > b",
 831            "    > C",
 832            "      .dockerignore",
 833            "v root2",
 834            "    > d",
 835            "    > e",
 836        ]
 837    );
 838
 839    // Add a file with the root folder selected. The filename editor is placed
 840    // before the first file in the root folder.
 841    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 842    panel.update_in(cx, |panel, window, cx| {
 843        assert!(panel.filename_editor.read(cx).is_focused(window));
 844    });
 845    assert_eq!(
 846        visible_entries_as_strings(&panel, 0..10, cx),
 847        &[
 848            "v root1",
 849            "    > .git",
 850            "    > a",
 851            "    > b",
 852            "    > C",
 853            "      [EDITOR: '']  <== selected",
 854            "      .dockerignore",
 855            "v root2",
 856            "    > d",
 857            "    > e",
 858        ]
 859    );
 860
 861    let confirm = panel.update_in(cx, |panel, window, cx| {
 862        panel.filename_editor.update(cx, |editor, cx| {
 863            editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
 864        });
 865        panel.confirm_edit(window, cx).unwrap()
 866    });
 867
 868    assert_eq!(
 869        visible_entries_as_strings(&panel, 0..10, cx),
 870        &[
 871            "v root1",
 872            "    > .git",
 873            "    > a",
 874            "    > b",
 875            "    > C",
 876            "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
 877            "      .dockerignore",
 878            "v root2",
 879            "    > d",
 880            "    > e",
 881        ]
 882    );
 883
 884    confirm.await.unwrap();
 885    assert_eq!(
 886        visible_entries_as_strings(&panel, 0..13, cx),
 887        &[
 888            "v root1",
 889            "    > .git",
 890            "    > a",
 891            "    > b",
 892            "    v bdir1",
 893            "        v dir2",
 894            "              the-new-filename  <== selected  <== marked",
 895            "    > C",
 896            "      .dockerignore",
 897            "v root2",
 898            "    > d",
 899            "    > e",
 900        ]
 901    );
 902}
 903
 904#[gpui::test]
 905async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
 906    init_test(cx);
 907
 908    let fs = FakeFs::new(cx.executor().clone());
 909    fs.insert_tree(
 910        path!("/root1"),
 911        json!({
 912            ".dockerignore": "",
 913            ".git": {
 914                "HEAD": "",
 915            },
 916        }),
 917    )
 918    .await;
 919
 920    let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
 921    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 922    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 923    let panel = workspace
 924        .update(cx, |workspace, window, cx| {
 925            let panel = ProjectPanel::new(workspace, window, cx);
 926            workspace.add_panel(panel.clone(), window, cx);
 927            panel
 928        })
 929        .unwrap();
 930
 931    select_path(&panel, "root1", cx);
 932    assert_eq!(
 933        visible_entries_as_strings(&panel, 0..10, cx),
 934        &["v root1  <== selected", "    > .git", "      .dockerignore",]
 935    );
 936
 937    // Add a file with the root folder selected. The filename editor is placed
 938    // before the first file in the root folder.
 939    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 940    panel.update_in(cx, |panel, window, cx| {
 941        assert!(panel.filename_editor.read(cx).is_focused(window));
 942    });
 943    assert_eq!(
 944        visible_entries_as_strings(&panel, 0..10, cx),
 945        &[
 946            "v root1",
 947            "    > .git",
 948            "      [EDITOR: '']  <== selected",
 949            "      .dockerignore",
 950        ]
 951    );
 952
 953    let confirm = panel.update_in(cx, |panel, window, cx| {
 954        // If we want to create a subdirectory, there should be no prefix slash.
 955        panel
 956            .filename_editor
 957            .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
 958        panel.confirm_edit(window, cx).unwrap()
 959    });
 960
 961    assert_eq!(
 962        visible_entries_as_strings(&panel, 0..10, cx),
 963        &[
 964            "v root1",
 965            "    > .git",
 966            "      [PROCESSING: 'new_dir/']  <== selected",
 967            "      .dockerignore",
 968        ]
 969    );
 970
 971    confirm.await.unwrap();
 972    assert_eq!(
 973        visible_entries_as_strings(&panel, 0..10, cx),
 974        &[
 975            "v root1",
 976            "    > .git",
 977            "    v new_dir  <== selected",
 978            "      .dockerignore",
 979        ]
 980    );
 981
 982    // Test filename with whitespace
 983    select_path(&panel, "root1", cx);
 984    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 985    let confirm = panel.update_in(cx, |panel, window, cx| {
 986        // If we want to create a subdirectory, there should be no prefix slash.
 987        panel
 988            .filename_editor
 989            .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
 990        panel.confirm_edit(window, cx).unwrap()
 991    });
 992    confirm.await.unwrap();
 993    assert_eq!(
 994        visible_entries_as_strings(&panel, 0..10, cx),
 995        &[
 996            "v root1",
 997            "    > .git",
 998            "    v new dir 2  <== selected",
 999            "    v new_dir",
1000            "      .dockerignore",
1001        ]
1002    );
1003
1004    // Test filename ends with "\"
1005    #[cfg(target_os = "windows")]
1006    {
1007        select_path(&panel, "root1", cx);
1008        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1009        let confirm = panel.update_in(cx, |panel, window, cx| {
1010            // If we want to create a subdirectory, there should be no prefix slash.
1011            panel
1012                .filename_editor
1013                .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
1014            panel.confirm_edit(window, cx).unwrap()
1015        });
1016        confirm.await.unwrap();
1017        assert_eq!(
1018            visible_entries_as_strings(&panel, 0..10, cx),
1019            &[
1020                "v root1",
1021                "    > .git",
1022                "    v new dir 2",
1023                "    v new_dir",
1024                "    v new_dir_3  <== selected",
1025                "      .dockerignore",
1026            ]
1027        );
1028    }
1029}
1030
1031#[gpui::test]
1032async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1033    init_test(cx);
1034
1035    let fs = FakeFs::new(cx.executor().clone());
1036    fs.insert_tree(
1037        "/root1",
1038        json!({
1039            "one.two.txt": "",
1040            "one.txt": ""
1041        }),
1042    )
1043    .await;
1044
1045    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1046    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1047    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1048    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1049
1050    panel.update_in(cx, |panel, window, cx| {
1051        panel.select_next(&Default::default(), window, cx);
1052        panel.select_next(&Default::default(), window, cx);
1053    });
1054
1055    assert_eq!(
1056        visible_entries_as_strings(&panel, 0..50, cx),
1057        &[
1058            //
1059            "v root1",
1060            "      one.txt  <== selected",
1061            "      one.two.txt",
1062        ]
1063    );
1064
1065    // Regression test - file name is created correctly when
1066    // the copied file's name contains multiple dots.
1067    panel.update_in(cx, |panel, window, cx| {
1068        panel.copy(&Default::default(), window, cx);
1069        panel.paste(&Default::default(), window, cx);
1070    });
1071    cx.executor().run_until_parked();
1072
1073    assert_eq!(
1074        visible_entries_as_strings(&panel, 0..50, cx),
1075        &[
1076            //
1077            "v root1",
1078            "      one.txt",
1079            "      [EDITOR: 'one copy.txt']  <== selected  <== marked",
1080            "      one.two.txt",
1081        ]
1082    );
1083
1084    panel.update_in(cx, |panel, window, cx| {
1085        panel.filename_editor.update(cx, |editor, cx| {
1086            let file_name_selections = editor.selections.all::<usize>(cx);
1087            assert_eq!(
1088                file_name_selections.len(),
1089                1,
1090                "File editing should have a single selection, but got: {file_name_selections:?}"
1091            );
1092            let file_name_selection = &file_name_selections[0];
1093            assert_eq!(
1094                file_name_selection.start,
1095                "one".len(),
1096                "Should select the file name disambiguation after the original file name"
1097            );
1098            assert_eq!(
1099                file_name_selection.end,
1100                "one copy".len(),
1101                "Should select the file name disambiguation until the extension"
1102            );
1103        });
1104        assert!(panel.confirm_edit(window, cx).is_none());
1105    });
1106
1107    panel.update_in(cx, |panel, window, cx| {
1108        panel.paste(&Default::default(), window, cx);
1109    });
1110    cx.executor().run_until_parked();
1111
1112    assert_eq!(
1113        visible_entries_as_strings(&panel, 0..50, cx),
1114        &[
1115            //
1116            "v root1",
1117            "      one.txt",
1118            "      one copy.txt",
1119            "      [EDITOR: 'one copy 1.txt']  <== selected  <== marked",
1120            "      one.two.txt",
1121        ]
1122    );
1123
1124    panel.update_in(cx, |panel, window, cx| {
1125        assert!(panel.confirm_edit(window, cx).is_none())
1126    });
1127}
1128
1129#[gpui::test]
1130async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1131    init_test(cx);
1132
1133    let fs = FakeFs::new(cx.executor().clone());
1134    fs.insert_tree(
1135        "/root1",
1136        json!({
1137            "one.txt": "",
1138            "two.txt": "",
1139            "three.txt": "",
1140            "a": {
1141                "0": { "q": "", "r": "", "s": "" },
1142                "1": { "t": "", "u": "" },
1143                "2": { "v": "", "w": "", "x": "", "y": "" },
1144            },
1145        }),
1146    )
1147    .await;
1148
1149    fs.insert_tree(
1150        "/root2",
1151        json!({
1152            "one.txt": "",
1153            "two.txt": "",
1154            "four.txt": "",
1155            "b": {
1156                "3": { "Q": "" },
1157                "4": { "R": "", "S": "", "T": "", "U": "" },
1158            },
1159        }),
1160    )
1161    .await;
1162
1163    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1164    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1165    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1166    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1167
1168    select_path(&panel, "root1/three.txt", cx);
1169    panel.update_in(cx, |panel, window, cx| {
1170        panel.cut(&Default::default(), window, cx);
1171    });
1172
1173    select_path(&panel, "root2/one.txt", cx);
1174    panel.update_in(cx, |panel, window, cx| {
1175        panel.select_next(&Default::default(), window, cx);
1176        panel.paste(&Default::default(), window, cx);
1177    });
1178    cx.executor().run_until_parked();
1179    assert_eq!(
1180        visible_entries_as_strings(&panel, 0..50, cx),
1181        &[
1182            //
1183            "v root1",
1184            "    > a",
1185            "      one.txt",
1186            "      two.txt",
1187            "v root2",
1188            "    > b",
1189            "      four.txt",
1190            "      one.txt",
1191            "      three.txt  <== selected  <== marked",
1192            "      two.txt",
1193        ]
1194    );
1195
1196    select_path(&panel, "root1/a", cx);
1197    panel.update_in(cx, |panel, window, cx| {
1198        panel.cut(&Default::default(), window, cx);
1199    });
1200    select_path(&panel, "root2/two.txt", cx);
1201    panel.update_in(cx, |panel, window, cx| {
1202        panel.select_next(&Default::default(), window, cx);
1203        panel.paste(&Default::default(), window, cx);
1204    });
1205
1206    cx.executor().run_until_parked();
1207    assert_eq!(
1208        visible_entries_as_strings(&panel, 0..50, cx),
1209        &[
1210            //
1211            "v root1",
1212            "      one.txt",
1213            "      two.txt",
1214            "v root2",
1215            "    > a  <== selected",
1216            "    > b",
1217            "      four.txt",
1218            "      one.txt",
1219            "      three.txt  <== marked",
1220            "      two.txt",
1221        ]
1222    );
1223}
1224
1225#[gpui::test]
1226async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1227    init_test(cx);
1228
1229    let fs = FakeFs::new(cx.executor().clone());
1230    fs.insert_tree(
1231        "/root1",
1232        json!({
1233            "one.txt": "",
1234            "two.txt": "",
1235            "three.txt": "",
1236            "a": {
1237                "0": { "q": "", "r": "", "s": "" },
1238                "1": { "t": "", "u": "" },
1239                "2": { "v": "", "w": "", "x": "", "y": "" },
1240            },
1241        }),
1242    )
1243    .await;
1244
1245    fs.insert_tree(
1246        "/root2",
1247        json!({
1248            "one.txt": "",
1249            "two.txt": "",
1250            "four.txt": "",
1251            "b": {
1252                "3": { "Q": "" },
1253                "4": { "R": "", "S": "", "T": "", "U": "" },
1254            },
1255        }),
1256    )
1257    .await;
1258
1259    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1260    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1261    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1262    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1263
1264    select_path(&panel, "root1/three.txt", cx);
1265    panel.update_in(cx, |panel, window, cx| {
1266        panel.copy(&Default::default(), window, cx);
1267    });
1268
1269    select_path(&panel, "root2/one.txt", cx);
1270    panel.update_in(cx, |panel, window, cx| {
1271        panel.select_next(&Default::default(), window, cx);
1272        panel.paste(&Default::default(), window, cx);
1273    });
1274    cx.executor().run_until_parked();
1275    assert_eq!(
1276        visible_entries_as_strings(&panel, 0..50, cx),
1277        &[
1278            //
1279            "v root1",
1280            "    > a",
1281            "      one.txt",
1282            "      three.txt",
1283            "      two.txt",
1284            "v root2",
1285            "    > b",
1286            "      four.txt",
1287            "      one.txt",
1288            "      three.txt  <== selected  <== marked",
1289            "      two.txt",
1290        ]
1291    );
1292
1293    select_path(&panel, "root1/three.txt", cx);
1294    panel.update_in(cx, |panel, window, cx| {
1295        panel.copy(&Default::default(), window, cx);
1296    });
1297    select_path(&panel, "root2/two.txt", cx);
1298    panel.update_in(cx, |panel, window, cx| {
1299        panel.select_next(&Default::default(), window, cx);
1300        panel.paste(&Default::default(), window, cx);
1301    });
1302
1303    cx.executor().run_until_parked();
1304    assert_eq!(
1305        visible_entries_as_strings(&panel, 0..50, cx),
1306        &[
1307            //
1308            "v root1",
1309            "    > a",
1310            "      one.txt",
1311            "      three.txt",
1312            "      two.txt",
1313            "v root2",
1314            "    > b",
1315            "      four.txt",
1316            "      one.txt",
1317            "      three.txt",
1318            "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
1319            "      two.txt",
1320        ]
1321    );
1322
1323    panel.update_in(cx, |panel, window, cx| {
1324        panel.cancel(&menu::Cancel {}, window, cx)
1325    });
1326    cx.executor().run_until_parked();
1327
1328    select_path(&panel, "root1/a", cx);
1329    panel.update_in(cx, |panel, window, cx| {
1330        panel.copy(&Default::default(), window, cx);
1331    });
1332    select_path(&panel, "root2/two.txt", cx);
1333    panel.update_in(cx, |panel, window, cx| {
1334        panel.select_next(&Default::default(), window, cx);
1335        panel.paste(&Default::default(), window, cx);
1336    });
1337
1338    cx.executor().run_until_parked();
1339    assert_eq!(
1340        visible_entries_as_strings(&panel, 0..50, cx),
1341        &[
1342            //
1343            "v root1",
1344            "    > a",
1345            "      one.txt",
1346            "      three.txt",
1347            "      two.txt",
1348            "v root2",
1349            "    > a  <== selected",
1350            "    > b",
1351            "      four.txt",
1352            "      one.txt",
1353            "      three.txt",
1354            "      three copy.txt",
1355            "      two.txt",
1356        ]
1357    );
1358}
1359
1360#[gpui::test]
1361async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1362    init_test(cx);
1363
1364    let fs = FakeFs::new(cx.executor().clone());
1365    fs.insert_tree(
1366        "/root",
1367        json!({
1368            "a": {
1369                "one.txt": "",
1370                "two.txt": "",
1371                "inner_dir": {
1372                    "three.txt": "",
1373                    "four.txt": "",
1374                }
1375            },
1376            "b": {}
1377        }),
1378    )
1379    .await;
1380
1381    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1382    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1383    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1384    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1385
1386    select_path(&panel, "root/a", cx);
1387    panel.update_in(cx, |panel, window, cx| {
1388        panel.copy(&Default::default(), window, cx);
1389        panel.select_next(&Default::default(), window, cx);
1390        panel.paste(&Default::default(), window, cx);
1391    });
1392    cx.executor().run_until_parked();
1393
1394    let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1395    assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1396
1397    let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1398    assert_ne!(
1399        pasted_dir_file, None,
1400        "Pasted directory file should have an entry"
1401    );
1402
1403    let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1404    assert_ne!(
1405        pasted_dir_inner_dir, None,
1406        "Directories inside pasted directory should have an entry"
1407    );
1408
1409    toggle_expand_dir(&panel, "root/b/a", cx);
1410    toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1411
1412    assert_eq!(
1413        visible_entries_as_strings(&panel, 0..50, cx),
1414        &[
1415            //
1416            "v root",
1417            "    > a",
1418            "    v b",
1419            "        v a",
1420            "            v inner_dir  <== selected",
1421            "                  four.txt",
1422            "                  three.txt",
1423            "              one.txt",
1424            "              two.txt",
1425        ]
1426    );
1427
1428    select_path(&panel, "root", cx);
1429    panel.update_in(cx, |panel, window, cx| {
1430        panel.paste(&Default::default(), window, cx)
1431    });
1432    cx.executor().run_until_parked();
1433    assert_eq!(
1434        visible_entries_as_strings(&panel, 0..50, cx),
1435        &[
1436            //
1437            "v root",
1438            "    > a",
1439            "    > [EDITOR: 'a copy']  <== selected",
1440            "    v b",
1441            "        v a",
1442            "            v inner_dir",
1443            "                  four.txt",
1444            "                  three.txt",
1445            "              one.txt",
1446            "              two.txt"
1447        ]
1448    );
1449
1450    let confirm = panel.update_in(cx, |panel, window, cx| {
1451        panel
1452            .filename_editor
1453            .update(cx, |editor, cx| editor.set_text("c", window, cx));
1454        panel.confirm_edit(window, cx).unwrap()
1455    });
1456    assert_eq!(
1457        visible_entries_as_strings(&panel, 0..50, cx),
1458        &[
1459            //
1460            "v root",
1461            "    > a",
1462            "    > [PROCESSING: 'c']  <== selected",
1463            "    v b",
1464            "        v a",
1465            "            v inner_dir",
1466            "                  four.txt",
1467            "                  three.txt",
1468            "              one.txt",
1469            "              two.txt"
1470        ]
1471    );
1472
1473    confirm.await.unwrap();
1474
1475    panel.update_in(cx, |panel, window, cx| {
1476        panel.paste(&Default::default(), window, cx)
1477    });
1478    cx.executor().run_until_parked();
1479    assert_eq!(
1480        visible_entries_as_strings(&panel, 0..50, cx),
1481        &[
1482            //
1483            "v root",
1484            "    > a",
1485            "    v b",
1486            "        v a",
1487            "            v inner_dir",
1488            "                  four.txt",
1489            "                  three.txt",
1490            "              one.txt",
1491            "              two.txt",
1492            "    v c",
1493            "        > a  <== selected",
1494            "        > inner_dir",
1495            "          one.txt",
1496            "          two.txt",
1497        ]
1498    );
1499}
1500
1501#[gpui::test]
1502async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1503    init_test(cx);
1504
1505    let fs = FakeFs::new(cx.executor().clone());
1506    fs.insert_tree(
1507        "/test",
1508        json!({
1509            "dir1": {
1510                "a.txt": "",
1511                "b.txt": "",
1512            },
1513            "dir2": {},
1514            "c.txt": "",
1515            "d.txt": "",
1516        }),
1517    )
1518    .await;
1519
1520    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1521    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1522    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1523    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1524
1525    toggle_expand_dir(&panel, "test/dir1", cx);
1526
1527    cx.simulate_modifiers_change(gpui::Modifiers {
1528        control: true,
1529        ..Default::default()
1530    });
1531
1532    select_path_with_mark(&panel, "test/dir1", cx);
1533    select_path_with_mark(&panel, "test/c.txt", cx);
1534
1535    assert_eq!(
1536        visible_entries_as_strings(&panel, 0..15, cx),
1537        &[
1538            "v test",
1539            "    v dir1  <== marked",
1540            "          a.txt",
1541            "          b.txt",
1542            "    > dir2",
1543            "      c.txt  <== selected  <== marked",
1544            "      d.txt",
1545        ],
1546        "Initial state before copying dir1 and c.txt"
1547    );
1548
1549    panel.update_in(cx, |panel, window, cx| {
1550        panel.copy(&Default::default(), window, cx);
1551    });
1552    select_path(&panel, "test/dir2", cx);
1553    panel.update_in(cx, |panel, window, cx| {
1554        panel.paste(&Default::default(), window, cx);
1555    });
1556    cx.executor().run_until_parked();
1557
1558    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1559
1560    assert_eq!(
1561        visible_entries_as_strings(&panel, 0..15, cx),
1562        &[
1563            "v test",
1564            "    v dir1  <== marked",
1565            "          a.txt",
1566            "          b.txt",
1567            "    v dir2",
1568            "        v dir1  <== selected",
1569            "              a.txt",
1570            "              b.txt",
1571            "          c.txt",
1572            "      c.txt  <== marked",
1573            "      d.txt",
1574        ],
1575        "Should copy dir1 as well as c.txt into dir2"
1576    );
1577
1578    // Disambiguating multiple files should not open the rename editor.
1579    select_path(&panel, "test/dir2", cx);
1580    panel.update_in(cx, |panel, window, cx| {
1581        panel.paste(&Default::default(), window, cx);
1582    });
1583    cx.executor().run_until_parked();
1584
1585    assert_eq!(
1586        visible_entries_as_strings(&panel, 0..15, cx),
1587        &[
1588            "v test",
1589            "    v dir1  <== marked",
1590            "          a.txt",
1591            "          b.txt",
1592            "    v dir2",
1593            "        v dir1",
1594            "              a.txt",
1595            "              b.txt",
1596            "        > dir1 copy  <== selected",
1597            "          c.txt",
1598            "          c copy.txt",
1599            "      c.txt  <== marked",
1600            "      d.txt",
1601        ],
1602        "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1603    );
1604}
1605
1606#[gpui::test]
1607async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1608    init_test(cx);
1609
1610    let fs = FakeFs::new(cx.executor().clone());
1611    fs.insert_tree(
1612        "/test",
1613        json!({
1614            "dir1": {
1615                "a.txt": "",
1616                "b.txt": "",
1617            },
1618            "dir2": {},
1619            "c.txt": "",
1620            "d.txt": "",
1621        }),
1622    )
1623    .await;
1624
1625    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1626    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1627    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1628    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1629
1630    toggle_expand_dir(&panel, "test/dir1", cx);
1631
1632    cx.simulate_modifiers_change(gpui::Modifiers {
1633        control: true,
1634        ..Default::default()
1635    });
1636
1637    select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1638    select_path_with_mark(&panel, "test/dir1", cx);
1639    select_path_with_mark(&panel, "test/c.txt", cx);
1640
1641    assert_eq!(
1642        visible_entries_as_strings(&panel, 0..15, cx),
1643        &[
1644            "v test",
1645            "    v dir1  <== marked",
1646            "          a.txt  <== marked",
1647            "          b.txt",
1648            "    > dir2",
1649            "      c.txt  <== selected  <== marked",
1650            "      d.txt",
1651        ],
1652        "Initial state before copying a.txt, dir1 and c.txt"
1653    );
1654
1655    panel.update_in(cx, |panel, window, cx| {
1656        panel.copy(&Default::default(), window, cx);
1657    });
1658    select_path(&panel, "test/dir2", cx);
1659    panel.update_in(cx, |panel, window, cx| {
1660        panel.paste(&Default::default(), window, cx);
1661    });
1662    cx.executor().run_until_parked();
1663
1664    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1665
1666    assert_eq!(
1667        visible_entries_as_strings(&panel, 0..20, cx),
1668        &[
1669            "v test",
1670            "    v dir1  <== marked",
1671            "          a.txt  <== marked",
1672            "          b.txt",
1673            "    v dir2",
1674            "        v dir1  <== selected",
1675            "              a.txt",
1676            "              b.txt",
1677            "          c.txt",
1678            "      c.txt  <== marked",
1679            "      d.txt",
1680        ],
1681        "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1682    );
1683}
1684
1685#[gpui::test]
1686async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1687    init_test_with_editor(cx);
1688
1689    let fs = FakeFs::new(cx.executor().clone());
1690    fs.insert_tree(
1691        path!("/src"),
1692        json!({
1693            "test": {
1694                "first.rs": "// First Rust file",
1695                "second.rs": "// Second Rust file",
1696                "third.rs": "// Third Rust file",
1697            }
1698        }),
1699    )
1700    .await;
1701
1702    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1703    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1704    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1705    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1706
1707    toggle_expand_dir(&panel, "src/test", cx);
1708    select_path(&panel, "src/test/first.rs", cx);
1709    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1710    cx.executor().run_until_parked();
1711    assert_eq!(
1712        visible_entries_as_strings(&panel, 0..10, cx),
1713        &[
1714            "v src",
1715            "    v test",
1716            "          first.rs  <== selected  <== marked",
1717            "          second.rs",
1718            "          third.rs"
1719        ]
1720    );
1721    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1722
1723    submit_deletion(&panel, cx);
1724    assert_eq!(
1725        visible_entries_as_strings(&panel, 0..10, cx),
1726        &[
1727            "v src",
1728            "    v test",
1729            "          second.rs  <== selected",
1730            "          third.rs"
1731        ],
1732        "Project panel should have no deleted file, no other file is selected in it"
1733    );
1734    ensure_no_open_items_and_panes(&workspace, cx);
1735
1736    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1737    cx.executor().run_until_parked();
1738    assert_eq!(
1739        visible_entries_as_strings(&panel, 0..10, cx),
1740        &[
1741            "v src",
1742            "    v test",
1743            "          second.rs  <== selected  <== marked",
1744            "          third.rs"
1745        ]
1746    );
1747    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1748
1749    workspace
1750        .update(cx, |workspace, window, cx| {
1751            let active_items = workspace
1752                .panes()
1753                .iter()
1754                .filter_map(|pane| pane.read(cx).active_item())
1755                .collect::<Vec<_>>();
1756            assert_eq!(active_items.len(), 1);
1757            let open_editor = active_items
1758                .into_iter()
1759                .next()
1760                .unwrap()
1761                .downcast::<Editor>()
1762                .expect("Open item should be an editor");
1763            open_editor.update(cx, |editor, cx| {
1764                editor.set_text("Another text!", window, cx)
1765            });
1766        })
1767        .unwrap();
1768    submit_deletion_skipping_prompt(&panel, cx);
1769    assert_eq!(
1770        visible_entries_as_strings(&panel, 0..10, cx),
1771        &["v src", "    v test", "          third.rs  <== selected"],
1772        "Project panel should have no deleted file, with one last file remaining"
1773    );
1774    ensure_no_open_items_and_panes(&workspace, cx);
1775}
1776
1777#[gpui::test]
1778async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
1779    init_test_with_editor(cx);
1780
1781    let fs = FakeFs::new(cx.executor().clone());
1782    fs.insert_tree(
1783        "/src",
1784        json!({
1785            "test": {
1786                "first.rs": "// First Rust file",
1787                "second.rs": "// Second Rust file",
1788                "third.rs": "// Third Rust file",
1789            }
1790        }),
1791    )
1792    .await;
1793
1794    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
1795    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1796    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1797    let panel = workspace
1798        .update(cx, |workspace, window, cx| {
1799            let panel = ProjectPanel::new(workspace, window, cx);
1800            workspace.add_panel(panel.clone(), window, cx);
1801            panel
1802        })
1803        .unwrap();
1804
1805    select_path(&panel, "src/", cx);
1806    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1807    cx.executor().run_until_parked();
1808    assert_eq!(
1809        visible_entries_as_strings(&panel, 0..10, cx),
1810        &[
1811            //
1812            "v src  <== selected",
1813            "    > test"
1814        ]
1815    );
1816    panel.update_in(cx, |panel, window, cx| {
1817        panel.new_directory(&NewDirectory, window, cx)
1818    });
1819    panel.update_in(cx, |panel, window, cx| {
1820        assert!(panel.filename_editor.read(cx).is_focused(window));
1821    });
1822    assert_eq!(
1823        visible_entries_as_strings(&panel, 0..10, cx),
1824        &[
1825            //
1826            "v src",
1827            "    > [EDITOR: '']  <== selected",
1828            "    > test"
1829        ]
1830    );
1831    panel.update_in(cx, |panel, window, cx| {
1832        panel
1833            .filename_editor
1834            .update(cx, |editor, cx| editor.set_text("test", window, cx));
1835        assert!(
1836            panel.confirm_edit(window, cx).is_none(),
1837            "Should not allow to confirm on conflicting new directory name"
1838        )
1839    });
1840    assert_eq!(
1841        visible_entries_as_strings(&panel, 0..10, cx),
1842        &[
1843            //
1844            "v src",
1845            "    > test"
1846        ],
1847        "File list should be unchanged after failed folder create confirmation"
1848    );
1849
1850    select_path(&panel, "src/test/", cx);
1851    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1852    cx.executor().run_until_parked();
1853    assert_eq!(
1854        visible_entries_as_strings(&panel, 0..10, cx),
1855        &[
1856            //
1857            "v src",
1858            "    > test  <== selected"
1859        ]
1860    );
1861    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1862    panel.update_in(cx, |panel, window, cx| {
1863        assert!(panel.filename_editor.read(cx).is_focused(window));
1864    });
1865    assert_eq!(
1866        visible_entries_as_strings(&panel, 0..10, cx),
1867        &[
1868            "v src",
1869            "    v test",
1870            "          [EDITOR: '']  <== selected",
1871            "          first.rs",
1872            "          second.rs",
1873            "          third.rs"
1874        ]
1875    );
1876    panel.update_in(cx, |panel, window, cx| {
1877        panel
1878            .filename_editor
1879            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
1880        assert!(
1881            panel.confirm_edit(window, cx).is_none(),
1882            "Should not allow to confirm on conflicting new file name"
1883        )
1884    });
1885    assert_eq!(
1886        visible_entries_as_strings(&panel, 0..10, cx),
1887        &[
1888            "v src",
1889            "    v test",
1890            "          first.rs",
1891            "          second.rs",
1892            "          third.rs"
1893        ],
1894        "File list should be unchanged after failed file create confirmation"
1895    );
1896
1897    select_path(&panel, "src/test/first.rs", cx);
1898    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1899    cx.executor().run_until_parked();
1900    assert_eq!(
1901        visible_entries_as_strings(&panel, 0..10, cx),
1902        &[
1903            "v src",
1904            "    v test",
1905            "          first.rs  <== selected",
1906            "          second.rs",
1907            "          third.rs"
1908        ],
1909    );
1910    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
1911    panel.update_in(cx, |panel, window, cx| {
1912        assert!(panel.filename_editor.read(cx).is_focused(window));
1913    });
1914    assert_eq!(
1915        visible_entries_as_strings(&panel, 0..10, cx),
1916        &[
1917            "v src",
1918            "    v test",
1919            "          [EDITOR: 'first.rs']  <== selected",
1920            "          second.rs",
1921            "          third.rs"
1922        ]
1923    );
1924    panel.update_in(cx, |panel, window, cx| {
1925        panel
1926            .filename_editor
1927            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
1928        assert!(
1929            panel.confirm_edit(window, cx).is_none(),
1930            "Should not allow to confirm on conflicting file rename"
1931        )
1932    });
1933    assert_eq!(
1934        visible_entries_as_strings(&panel, 0..10, cx),
1935        &[
1936            "v src",
1937            "    v test",
1938            "          first.rs  <== selected",
1939            "          second.rs",
1940            "          third.rs"
1941        ],
1942        "File list should be unchanged after failed rename confirmation"
1943    );
1944}
1945
1946#[gpui::test]
1947async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
1948    init_test_with_editor(cx);
1949
1950    let fs = FakeFs::new(cx.executor().clone());
1951    fs.insert_tree(
1952        path!("/root"),
1953        json!({
1954            "tree1": {
1955                ".git": {},
1956                "dir1": {
1957                    "modified1.txt": "1",
1958                    "unmodified1.txt": "1",
1959                    "modified2.txt": "1",
1960                },
1961                "dir2": {
1962                    "modified3.txt": "1",
1963                    "unmodified2.txt": "1",
1964                },
1965                "modified4.txt": "1",
1966                "unmodified3.txt": "1",
1967            },
1968            "tree2": {
1969                ".git": {},
1970                "dir3": {
1971                    "modified5.txt": "1",
1972                    "unmodified4.txt": "1",
1973                },
1974                "modified6.txt": "1",
1975                "unmodified5.txt": "1",
1976            }
1977        }),
1978    )
1979    .await;
1980
1981    // Mark files as git modified
1982    fs.set_git_content_for_repo(
1983        path!("/root/tree1/.git").as_ref(),
1984        &[
1985            ("dir1/modified1.txt".into(), "modified".into(), None),
1986            ("dir1/modified2.txt".into(), "modified".into(), None),
1987            ("modified4.txt".into(), "modified".into(), None),
1988            ("dir2/modified3.txt".into(), "modified".into(), None),
1989        ],
1990    );
1991    fs.set_git_content_for_repo(
1992        path!("/root/tree2/.git").as_ref(),
1993        &[
1994            ("dir3/modified5.txt".into(), "modified".into(), None),
1995            ("modified6.txt".into(), "modified".into(), None),
1996        ],
1997    );
1998
1999    let project = Project::test(
2000        fs.clone(),
2001        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2002        cx,
2003    )
2004    .await;
2005
2006    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2007        let mut worktrees = project.worktrees(cx);
2008        let worktree1 = worktrees.next().unwrap();
2009        let worktree2 = worktrees.next().unwrap();
2010        (
2011            worktree1.read(cx).as_local().unwrap().scan_complete(),
2012            worktree2.read(cx).as_local().unwrap().scan_complete(),
2013        )
2014    });
2015    scan1_complete.await;
2016    scan2_complete.await;
2017    cx.run_until_parked();
2018
2019    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2020    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2021    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2022
2023    // Check initial state
2024    assert_eq!(
2025        visible_entries_as_strings(&panel, 0..15, cx),
2026        &[
2027            "v tree1",
2028            "    > .git",
2029            "    > dir1",
2030            "    > dir2",
2031            "      modified4.txt",
2032            "      unmodified3.txt",
2033            "v tree2",
2034            "    > .git",
2035            "    > dir3",
2036            "      modified6.txt",
2037            "      unmodified5.txt"
2038        ],
2039    );
2040
2041    // Test selecting next modified entry
2042    panel.update_in(cx, |panel, window, cx| {
2043        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2044    });
2045
2046    assert_eq!(
2047        visible_entries_as_strings(&panel, 0..6, cx),
2048        &[
2049            "v tree1",
2050            "    > .git",
2051            "    v dir1",
2052            "          modified1.txt  <== selected",
2053            "          modified2.txt",
2054            "          unmodified1.txt",
2055        ],
2056    );
2057
2058    panel.update_in(cx, |panel, window, cx| {
2059        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2060    });
2061
2062    assert_eq!(
2063        visible_entries_as_strings(&panel, 0..6, cx),
2064        &[
2065            "v tree1",
2066            "    > .git",
2067            "    v dir1",
2068            "          modified1.txt",
2069            "          modified2.txt  <== selected",
2070            "          unmodified1.txt",
2071        ],
2072    );
2073
2074    panel.update_in(cx, |panel, window, cx| {
2075        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2076    });
2077
2078    assert_eq!(
2079        visible_entries_as_strings(&panel, 6..9, cx),
2080        &[
2081            "    v dir2",
2082            "          modified3.txt  <== selected",
2083            "          unmodified2.txt",
2084        ],
2085    );
2086
2087    panel.update_in(cx, |panel, window, cx| {
2088        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2089    });
2090
2091    assert_eq!(
2092        visible_entries_as_strings(&panel, 9..11, cx),
2093        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2094    );
2095
2096    panel.update_in(cx, |panel, window, cx| {
2097        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2098    });
2099
2100    assert_eq!(
2101        visible_entries_as_strings(&panel, 13..16, cx),
2102        &[
2103            "    v dir3",
2104            "          modified5.txt  <== selected",
2105            "          unmodified4.txt",
2106        ],
2107    );
2108
2109    panel.update_in(cx, |panel, window, cx| {
2110        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2111    });
2112
2113    assert_eq!(
2114        visible_entries_as_strings(&panel, 16..18, cx),
2115        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2116    );
2117
2118    // Wraps around to first modified file
2119    panel.update_in(cx, |panel, window, cx| {
2120        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2121    });
2122
2123    assert_eq!(
2124        visible_entries_as_strings(&panel, 0..18, cx),
2125        &[
2126            "v tree1",
2127            "    > .git",
2128            "    v dir1",
2129            "          modified1.txt  <== selected",
2130            "          modified2.txt",
2131            "          unmodified1.txt",
2132            "    v dir2",
2133            "          modified3.txt",
2134            "          unmodified2.txt",
2135            "      modified4.txt",
2136            "      unmodified3.txt",
2137            "v tree2",
2138            "    > .git",
2139            "    v dir3",
2140            "          modified5.txt",
2141            "          unmodified4.txt",
2142            "      modified6.txt",
2143            "      unmodified5.txt",
2144        ],
2145    );
2146
2147    // Wraps around again to last modified file
2148    panel.update_in(cx, |panel, window, cx| {
2149        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2150    });
2151
2152    assert_eq!(
2153        visible_entries_as_strings(&panel, 16..18, cx),
2154        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2155    );
2156
2157    panel.update_in(cx, |panel, window, cx| {
2158        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2159    });
2160
2161    assert_eq!(
2162        visible_entries_as_strings(&panel, 13..16, cx),
2163        &[
2164            "    v dir3",
2165            "          modified5.txt  <== selected",
2166            "          unmodified4.txt",
2167        ],
2168    );
2169
2170    panel.update_in(cx, |panel, window, cx| {
2171        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2172    });
2173
2174    assert_eq!(
2175        visible_entries_as_strings(&panel, 9..11, cx),
2176        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2177    );
2178
2179    panel.update_in(cx, |panel, window, cx| {
2180        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2181    });
2182
2183    assert_eq!(
2184        visible_entries_as_strings(&panel, 6..9, cx),
2185        &[
2186            "    v dir2",
2187            "          modified3.txt  <== selected",
2188            "          unmodified2.txt",
2189        ],
2190    );
2191
2192    panel.update_in(cx, |panel, window, cx| {
2193        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2194    });
2195
2196    assert_eq!(
2197        visible_entries_as_strings(&panel, 0..6, cx),
2198        &[
2199            "v tree1",
2200            "    > .git",
2201            "    v dir1",
2202            "          modified1.txt",
2203            "          modified2.txt  <== selected",
2204            "          unmodified1.txt",
2205        ],
2206    );
2207
2208    panel.update_in(cx, |panel, window, cx| {
2209        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2210    });
2211
2212    assert_eq!(
2213        visible_entries_as_strings(&panel, 0..6, cx),
2214        &[
2215            "v tree1",
2216            "    > .git",
2217            "    v dir1",
2218            "          modified1.txt  <== selected",
2219            "          modified2.txt",
2220            "          unmodified1.txt",
2221        ],
2222    );
2223}
2224
2225#[gpui::test]
2226async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2227    init_test_with_editor(cx);
2228
2229    let fs = FakeFs::new(cx.executor().clone());
2230    fs.insert_tree(
2231        "/project_root",
2232        json!({
2233            "dir_1": {
2234                "nested_dir": {
2235                    "file_a.py": "# File contents",
2236                }
2237            },
2238            "file_1.py": "# File contents",
2239            "dir_2": {
2240
2241            },
2242            "dir_3": {
2243
2244            },
2245            "file_2.py": "# File contents",
2246            "dir_4": {
2247
2248            },
2249        }),
2250    )
2251    .await;
2252
2253    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2254    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2255    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2256    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2257
2258    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2259    cx.executor().run_until_parked();
2260    select_path(&panel, "project_root/dir_1", cx);
2261    cx.executor().run_until_parked();
2262    assert_eq!(
2263        visible_entries_as_strings(&panel, 0..10, cx),
2264        &[
2265            "v project_root",
2266            "    > dir_1  <== selected",
2267            "    > dir_2",
2268            "    > dir_3",
2269            "    > dir_4",
2270            "      file_1.py",
2271            "      file_2.py",
2272        ]
2273    );
2274    panel.update_in(cx, |panel, window, cx| {
2275        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2276    });
2277
2278    assert_eq!(
2279        visible_entries_as_strings(&panel, 0..10, cx),
2280        &[
2281            "v project_root  <== selected",
2282            "    > dir_1",
2283            "    > dir_2",
2284            "    > dir_3",
2285            "    > dir_4",
2286            "      file_1.py",
2287            "      file_2.py",
2288        ]
2289    );
2290
2291    panel.update_in(cx, |panel, window, cx| {
2292        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2293    });
2294
2295    assert_eq!(
2296        visible_entries_as_strings(&panel, 0..10, cx),
2297        &[
2298            "v project_root",
2299            "    > dir_1",
2300            "    > dir_2",
2301            "    > dir_3",
2302            "    > dir_4  <== selected",
2303            "      file_1.py",
2304            "      file_2.py",
2305        ]
2306    );
2307
2308    panel.update_in(cx, |panel, window, cx| {
2309        panel.select_next_directory(&SelectNextDirectory, window, cx)
2310    });
2311
2312    assert_eq!(
2313        visible_entries_as_strings(&panel, 0..10, cx),
2314        &[
2315            "v project_root  <== selected",
2316            "    > dir_1",
2317            "    > dir_2",
2318            "    > dir_3",
2319            "    > dir_4",
2320            "      file_1.py",
2321            "      file_2.py",
2322        ]
2323    );
2324}
2325#[gpui::test]
2326async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2327    init_test_with_editor(cx);
2328
2329    let fs = FakeFs::new(cx.executor().clone());
2330    fs.insert_tree(
2331        "/project_root",
2332        json!({
2333            "dir_1": {
2334                "nested_dir": {
2335                    "file_a.py": "# File contents",
2336                }
2337            },
2338            "file_1.py": "# File contents",
2339            "file_2.py": "# File contents",
2340            "zdir_2": {
2341                "nested_dir2": {
2342                    "file_b.py": "# File contents",
2343                }
2344            },
2345        }),
2346    )
2347    .await;
2348
2349    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2350    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2351    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2352    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2353
2354    assert_eq!(
2355        visible_entries_as_strings(&panel, 0..10, cx),
2356        &[
2357            "v project_root",
2358            "    > dir_1",
2359            "    > zdir_2",
2360            "      file_1.py",
2361            "      file_2.py",
2362        ]
2363    );
2364    panel.update_in(cx, |panel, window, cx| {
2365        panel.select_first(&SelectFirst, window, cx)
2366    });
2367
2368    assert_eq!(
2369        visible_entries_as_strings(&panel, 0..10, cx),
2370        &[
2371            "v project_root  <== selected",
2372            "    > dir_1",
2373            "    > zdir_2",
2374            "      file_1.py",
2375            "      file_2.py",
2376        ]
2377    );
2378
2379    panel.update_in(cx, |panel, window, cx| {
2380        panel.select_last(&SelectLast, window, cx)
2381    });
2382
2383    assert_eq!(
2384        visible_entries_as_strings(&panel, 0..10, cx),
2385        &[
2386            "v project_root",
2387            "    > dir_1",
2388            "    > zdir_2",
2389            "      file_1.py",
2390            "      file_2.py  <== selected",
2391        ]
2392    );
2393}
2394
2395#[gpui::test]
2396async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2397    init_test_with_editor(cx);
2398
2399    let fs = FakeFs::new(cx.executor().clone());
2400    fs.insert_tree(
2401        "/project_root",
2402        json!({
2403            "dir_1": {
2404                "nested_dir": {
2405                    "file_a.py": "# File contents",
2406                }
2407            },
2408            "file_1.py": "# File contents",
2409        }),
2410    )
2411    .await;
2412
2413    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2414    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2415    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2416    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2417
2418    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2419    cx.executor().run_until_parked();
2420    select_path(&panel, "project_root/dir_1", cx);
2421    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2422    select_path(&panel, "project_root/dir_1/nested_dir", cx);
2423    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2424    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2425    cx.executor().run_until_parked();
2426    assert_eq!(
2427        visible_entries_as_strings(&panel, 0..10, cx),
2428        &[
2429            "v project_root",
2430            "    v dir_1",
2431            "        > nested_dir  <== selected",
2432            "      file_1.py",
2433        ]
2434    );
2435}
2436
2437#[gpui::test]
2438async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2439    init_test_with_editor(cx);
2440
2441    let fs = FakeFs::new(cx.executor().clone());
2442    fs.insert_tree(
2443        "/project_root",
2444        json!({
2445            "dir_1": {
2446                "nested_dir": {
2447                    "file_a.py": "# File contents",
2448                    "file_b.py": "# File contents",
2449                    "file_c.py": "# File contents",
2450                },
2451                "file_1.py": "# File contents",
2452                "file_2.py": "# File contents",
2453                "file_3.py": "# File contents",
2454            },
2455            "dir_2": {
2456                "file_1.py": "# File contents",
2457                "file_2.py": "# File contents",
2458                "file_3.py": "# File contents",
2459            }
2460        }),
2461    )
2462    .await;
2463
2464    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2465    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2466    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2467    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2468
2469    panel.update_in(cx, |panel, window, cx| {
2470        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2471    });
2472    cx.executor().run_until_parked();
2473    assert_eq!(
2474        visible_entries_as_strings(&panel, 0..10, cx),
2475        &["v project_root", "    > dir_1", "    > dir_2",]
2476    );
2477
2478    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2479    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2480    cx.executor().run_until_parked();
2481    assert_eq!(
2482        visible_entries_as_strings(&panel, 0..10, cx),
2483        &[
2484            "v project_root",
2485            "    v dir_1  <== selected",
2486            "        > nested_dir",
2487            "          file_1.py",
2488            "          file_2.py",
2489            "          file_3.py",
2490            "    > dir_2",
2491        ]
2492    );
2493}
2494
2495#[gpui::test]
2496async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2497    init_test(cx);
2498
2499    let fs = FakeFs::new(cx.executor().clone());
2500    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2501    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2502    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2503    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2504    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2505
2506    // Make a new buffer with no backing file
2507    workspace
2508        .update(cx, |workspace, window, cx| {
2509            Editor::new_file(workspace, &Default::default(), window, cx)
2510        })
2511        .unwrap();
2512
2513    cx.executor().run_until_parked();
2514
2515    // "Save as" the buffer, creating a new backing file for it
2516    let save_task = workspace
2517        .update(cx, |workspace, window, cx| {
2518            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2519        })
2520        .unwrap();
2521
2522    cx.executor().run_until_parked();
2523    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2524    save_task.await.unwrap();
2525
2526    // Rename the file
2527    select_path(&panel, "root/new", cx);
2528    assert_eq!(
2529        visible_entries_as_strings(&panel, 0..10, cx),
2530        &["v root", "      new  <== selected  <== marked"]
2531    );
2532    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2533    panel.update_in(cx, |panel, window, cx| {
2534        panel
2535            .filename_editor
2536            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2537    });
2538    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2539
2540    cx.executor().run_until_parked();
2541    assert_eq!(
2542        visible_entries_as_strings(&panel, 0..10, cx),
2543        &["v root", "      newer  <== selected"]
2544    );
2545
2546    workspace
2547        .update(cx, |workspace, window, cx| {
2548            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2549        })
2550        .unwrap()
2551        .await
2552        .unwrap();
2553
2554    cx.executor().run_until_parked();
2555    // assert that saving the file doesn't restore "new"
2556    assert_eq!(
2557        visible_entries_as_strings(&panel, 0..10, cx),
2558        &["v root", "      newer  <== selected"]
2559    );
2560}
2561
2562#[gpui::test]
2563#[cfg_attr(target_os = "windows", ignore)]
2564async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2565    init_test_with_editor(cx);
2566
2567    let fs = FakeFs::new(cx.executor().clone());
2568    fs.insert_tree(
2569        "/root1",
2570        json!({
2571            "dir1": {
2572                "file1.txt": "content 1",
2573            },
2574        }),
2575    )
2576    .await;
2577
2578    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2579    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2580    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2581    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2582
2583    toggle_expand_dir(&panel, "root1/dir1", cx);
2584
2585    assert_eq!(
2586        visible_entries_as_strings(&panel, 0..20, cx),
2587        &["v root1", "    v dir1  <== selected", "          file1.txt",],
2588        "Initial state with worktrees"
2589    );
2590
2591    select_path(&panel, "root1", cx);
2592    assert_eq!(
2593        visible_entries_as_strings(&panel, 0..20, cx),
2594        &["v root1  <== selected", "    v dir1", "          file1.txt",],
2595    );
2596
2597    // Rename root1 to new_root1
2598    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2599
2600    assert_eq!(
2601        visible_entries_as_strings(&panel, 0..20, cx),
2602        &[
2603            "v [EDITOR: 'root1']  <== selected",
2604            "    v dir1",
2605            "          file1.txt",
2606        ],
2607    );
2608
2609    let confirm = panel.update_in(cx, |panel, window, cx| {
2610        panel
2611            .filename_editor
2612            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2613        panel.confirm_edit(window, cx).unwrap()
2614    });
2615    confirm.await.unwrap();
2616    assert_eq!(
2617        visible_entries_as_strings(&panel, 0..20, cx),
2618        &[
2619            "v new_root1  <== selected",
2620            "    v dir1",
2621            "          file1.txt",
2622        ],
2623        "Should update worktree name"
2624    );
2625
2626    // Ensure internal paths have been updated
2627    select_path(&panel, "new_root1/dir1/file1.txt", cx);
2628    assert_eq!(
2629        visible_entries_as_strings(&panel, 0..20, cx),
2630        &[
2631            "v new_root1",
2632            "    v dir1",
2633            "          file1.txt  <== selected",
2634        ],
2635        "Files in renamed worktree are selectable"
2636    );
2637}
2638
2639#[gpui::test]
2640async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2641    init_test_with_editor(cx);
2642    let fs = FakeFs::new(cx.executor().clone());
2643    fs.insert_tree(
2644        "/project_root",
2645        json!({
2646            "dir_1": {
2647                "nested_dir": {
2648                    "file_a.py": "# File contents",
2649                }
2650            },
2651            "file_1.py": "# File contents",
2652        }),
2653    )
2654    .await;
2655
2656    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2657    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2658    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2659    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2660    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2661    cx.update(|window, cx| {
2662        panel.update(cx, |this, cx| {
2663            this.select_next(&Default::default(), window, cx);
2664            this.expand_selected_entry(&Default::default(), window, cx);
2665            this.expand_selected_entry(&Default::default(), window, cx);
2666            this.select_next(&Default::default(), window, cx);
2667            this.expand_selected_entry(&Default::default(), window, cx);
2668            this.select_next(&Default::default(), window, cx);
2669        })
2670    });
2671    assert_eq!(
2672        visible_entries_as_strings(&panel, 0..10, cx),
2673        &[
2674            "v project_root",
2675            "    v dir_1",
2676            "        v nested_dir",
2677            "              file_a.py  <== selected",
2678            "      file_1.py",
2679        ]
2680    );
2681    let modifiers_with_shift = gpui::Modifiers {
2682        shift: true,
2683        ..Default::default()
2684    };
2685    cx.simulate_modifiers_change(modifiers_with_shift);
2686    cx.update(|window, cx| {
2687        panel.update(cx, |this, cx| {
2688            this.select_next(&Default::default(), window, cx);
2689        })
2690    });
2691    assert_eq!(
2692        visible_entries_as_strings(&panel, 0..10, cx),
2693        &[
2694            "v project_root",
2695            "    v dir_1",
2696            "        v nested_dir",
2697            "              file_a.py",
2698            "      file_1.py  <== selected  <== marked",
2699        ]
2700    );
2701    cx.update(|window, cx| {
2702        panel.update(cx, |this, cx| {
2703            this.select_previous(&Default::default(), window, cx);
2704        })
2705    });
2706    assert_eq!(
2707        visible_entries_as_strings(&panel, 0..10, cx),
2708        &[
2709            "v project_root",
2710            "    v dir_1",
2711            "        v nested_dir",
2712            "              file_a.py  <== selected  <== marked",
2713            "      file_1.py  <== marked",
2714        ]
2715    );
2716    cx.update(|window, cx| {
2717        panel.update(cx, |this, cx| {
2718            let drag = DraggedSelection {
2719                active_selection: this.selection.unwrap(),
2720                marked_selections: Arc::new(this.marked_entries.clone()),
2721            };
2722            let target_entry = this
2723                .project
2724                .read(cx)
2725                .entry_for_path(&(worktree_id, "").into(), cx)
2726                .unwrap();
2727            this.drag_onto(&drag, target_entry.id, false, window, cx);
2728        });
2729    });
2730    cx.run_until_parked();
2731    assert_eq!(
2732        visible_entries_as_strings(&panel, 0..10, cx),
2733        &[
2734            "v project_root",
2735            "    v dir_1",
2736            "        v nested_dir",
2737            "      file_1.py  <== marked",
2738            "      file_a.py  <== selected  <== marked",
2739        ]
2740    );
2741    // ESC clears out all marks
2742    cx.update(|window, cx| {
2743        panel.update(cx, |this, cx| {
2744            this.cancel(&menu::Cancel, window, cx);
2745        })
2746    });
2747    assert_eq!(
2748        visible_entries_as_strings(&panel, 0..10, cx),
2749        &[
2750            "v project_root",
2751            "    v dir_1",
2752            "        v nested_dir",
2753            "      file_1.py",
2754            "      file_a.py  <== selected",
2755        ]
2756    );
2757    // ESC clears out all marks
2758    cx.update(|window, cx| {
2759        panel.update(cx, |this, cx| {
2760            this.select_previous(&SelectPrevious, window, cx);
2761            this.select_next(&SelectNext, window, cx);
2762        })
2763    });
2764    assert_eq!(
2765        visible_entries_as_strings(&panel, 0..10, cx),
2766        &[
2767            "v project_root",
2768            "    v dir_1",
2769            "        v nested_dir",
2770            "      file_1.py  <== marked",
2771            "      file_a.py  <== selected  <== marked",
2772        ]
2773    );
2774    cx.simulate_modifiers_change(Default::default());
2775    cx.update(|window, cx| {
2776        panel.update(cx, |this, cx| {
2777            this.cut(&Cut, window, cx);
2778            this.select_previous(&SelectPrevious, window, cx);
2779            this.select_previous(&SelectPrevious, window, cx);
2780
2781            this.paste(&Paste, window, cx);
2782            // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2783        })
2784    });
2785    cx.run_until_parked();
2786    assert_eq!(
2787        visible_entries_as_strings(&panel, 0..10, cx),
2788        &[
2789            "v project_root",
2790            "    v dir_1",
2791            "        v nested_dir",
2792            "              file_1.py  <== marked",
2793            "              file_a.py  <== selected  <== marked",
2794        ]
2795    );
2796    cx.simulate_modifiers_change(modifiers_with_shift);
2797    cx.update(|window, cx| {
2798        panel.update(cx, |this, cx| {
2799            this.expand_selected_entry(&Default::default(), window, cx);
2800            this.select_next(&SelectNext, window, cx);
2801            this.select_next(&SelectNext, window, cx);
2802        })
2803    });
2804    submit_deletion(&panel, cx);
2805    assert_eq!(
2806        visible_entries_as_strings(&panel, 0..10, cx),
2807        &[
2808            "v project_root",
2809            "    v dir_1",
2810            "        v nested_dir  <== selected",
2811        ]
2812    );
2813}
2814#[gpui::test]
2815async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2816    init_test_with_editor(cx);
2817    cx.update(|cx| {
2818        cx.update_global::<SettingsStore, _>(|store, cx| {
2819            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2820                worktree_settings.file_scan_exclusions = Some(Vec::new());
2821            });
2822            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2823                project_panel_settings.auto_reveal_entries = Some(false)
2824            });
2825        })
2826    });
2827
2828    let fs = FakeFs::new(cx.background_executor.clone());
2829    fs.insert_tree(
2830        "/project_root",
2831        json!({
2832            ".git": {},
2833            ".gitignore": "**/gitignored_dir",
2834            "dir_1": {
2835                "file_1.py": "# File 1_1 contents",
2836                "file_2.py": "# File 1_2 contents",
2837                "file_3.py": "# File 1_3 contents",
2838                "gitignored_dir": {
2839                    "file_a.py": "# File contents",
2840                    "file_b.py": "# File contents",
2841                    "file_c.py": "# File contents",
2842                },
2843            },
2844            "dir_2": {
2845                "file_1.py": "# File 2_1 contents",
2846                "file_2.py": "# File 2_2 contents",
2847                "file_3.py": "# File 2_3 contents",
2848            }
2849        }),
2850    )
2851    .await;
2852
2853    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2854    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2855    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2856    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2857
2858    assert_eq!(
2859        visible_entries_as_strings(&panel, 0..20, cx),
2860        &[
2861            "v project_root",
2862            "    > .git",
2863            "    > dir_1",
2864            "    > dir_2",
2865            "      .gitignore",
2866        ]
2867    );
2868
2869    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
2870        .expect("dir 1 file is not ignored and should have an entry");
2871    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
2872        .expect("dir 2 file is not ignored and should have an entry");
2873    let gitignored_dir_file =
2874        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
2875    assert_eq!(
2876        gitignored_dir_file, None,
2877        "File in the gitignored dir should not have an entry before its dir is toggled"
2878    );
2879
2880    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2881    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2882    cx.executor().run_until_parked();
2883    assert_eq!(
2884        visible_entries_as_strings(&panel, 0..20, cx),
2885        &[
2886            "v project_root",
2887            "    > .git",
2888            "    v dir_1",
2889            "        v gitignored_dir  <== selected",
2890            "              file_a.py",
2891            "              file_b.py",
2892            "              file_c.py",
2893            "          file_1.py",
2894            "          file_2.py",
2895            "          file_3.py",
2896            "    > dir_2",
2897            "      .gitignore",
2898        ],
2899        "Should show gitignored dir file list in the project panel"
2900    );
2901    let gitignored_dir_file =
2902        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
2903            .expect("after gitignored dir got opened, a file entry should be present");
2904
2905    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2906    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2907    assert_eq!(
2908        visible_entries_as_strings(&panel, 0..20, cx),
2909        &[
2910            "v project_root",
2911            "    > .git",
2912            "    > dir_1  <== selected",
2913            "    > dir_2",
2914            "      .gitignore",
2915        ],
2916        "Should hide all dir contents again and prepare for the auto reveal test"
2917    );
2918
2919    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
2920        panel.update(cx, |panel, cx| {
2921            panel.project.update(cx, |_, cx| {
2922                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
2923            })
2924        });
2925        cx.run_until_parked();
2926        assert_eq!(
2927            visible_entries_as_strings(&panel, 0..20, cx),
2928            &[
2929                "v project_root",
2930                "    > .git",
2931                "    > dir_1  <== selected",
2932                "    > dir_2",
2933                "      .gitignore",
2934            ],
2935            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
2936        );
2937    }
2938
2939    cx.update(|_, cx| {
2940        cx.update_global::<SettingsStore, _>(|store, cx| {
2941            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2942                project_panel_settings.auto_reveal_entries = Some(true)
2943            });
2944        })
2945    });
2946
2947    panel.update(cx, |panel, cx| {
2948        panel.project.update(cx, |_, cx| {
2949            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
2950        })
2951    });
2952    cx.run_until_parked();
2953    assert_eq!(
2954        visible_entries_as_strings(&panel, 0..20, cx),
2955        &[
2956            "v project_root",
2957            "    > .git",
2958            "    v dir_1",
2959            "        > gitignored_dir",
2960            "          file_1.py  <== selected  <== marked",
2961            "          file_2.py",
2962            "          file_3.py",
2963            "    > dir_2",
2964            "      .gitignore",
2965        ],
2966        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
2967    );
2968
2969    panel.update(cx, |panel, cx| {
2970        panel.project.update(cx, |_, cx| {
2971            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
2972        })
2973    });
2974    cx.run_until_parked();
2975    assert_eq!(
2976        visible_entries_as_strings(&panel, 0..20, cx),
2977        &[
2978            "v project_root",
2979            "    > .git",
2980            "    v dir_1",
2981            "        > gitignored_dir",
2982            "          file_1.py",
2983            "          file_2.py",
2984            "          file_3.py",
2985            "    v dir_2",
2986            "          file_1.py  <== selected  <== marked",
2987            "          file_2.py",
2988            "          file_3.py",
2989            "      .gitignore",
2990        ],
2991        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
2992    );
2993
2994    panel.update(cx, |panel, cx| {
2995        panel.project.update(cx, |_, cx| {
2996            cx.emit(project::Event::ActiveEntryChanged(Some(
2997                gitignored_dir_file,
2998            )))
2999        })
3000    });
3001    cx.run_until_parked();
3002    assert_eq!(
3003        visible_entries_as_strings(&panel, 0..20, cx),
3004        &[
3005            "v project_root",
3006            "    > .git",
3007            "    v dir_1",
3008            "        > gitignored_dir",
3009            "          file_1.py",
3010            "          file_2.py",
3011            "          file_3.py",
3012            "    v dir_2",
3013            "          file_1.py  <== selected  <== marked",
3014            "          file_2.py",
3015            "          file_3.py",
3016            "      .gitignore",
3017        ],
3018        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3019    );
3020
3021    panel.update(cx, |panel, cx| {
3022        panel.project.update(cx, |_, cx| {
3023            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3024        })
3025    });
3026    cx.run_until_parked();
3027    assert_eq!(
3028        visible_entries_as_strings(&panel, 0..20, cx),
3029        &[
3030            "v project_root",
3031            "    > .git",
3032            "    v dir_1",
3033            "        v gitignored_dir",
3034            "              file_a.py  <== selected  <== marked",
3035            "              file_b.py",
3036            "              file_c.py",
3037            "          file_1.py",
3038            "          file_2.py",
3039            "          file_3.py",
3040            "    v dir_2",
3041            "          file_1.py",
3042            "          file_2.py",
3043            "          file_3.py",
3044            "      .gitignore",
3045        ],
3046        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3047    );
3048}
3049
3050#[gpui::test]
3051async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3052    init_test_with_editor(cx);
3053    cx.update(|cx| {
3054        cx.update_global::<SettingsStore, _>(|store, cx| {
3055            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3056                worktree_settings.file_scan_exclusions = Some(Vec::new());
3057                worktree_settings.file_scan_inclusions =
3058                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3059            });
3060            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3061                project_panel_settings.auto_reveal_entries = Some(false)
3062            });
3063        })
3064    });
3065
3066    let fs = FakeFs::new(cx.background_executor.clone());
3067    fs.insert_tree(
3068        "/project_root",
3069        json!({
3070            ".git": {},
3071            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3072            "dir_1": {
3073                "file_1.py": "# File 1_1 contents",
3074                "file_2.py": "# File 1_2 contents",
3075                "file_3.py": "# File 1_3 contents",
3076                "gitignored_dir": {
3077                    "file_a.py": "# File contents",
3078                    "file_b.py": "# File contents",
3079                    "file_c.py": "# File contents",
3080                },
3081            },
3082            "dir_2": {
3083                "file_1.py": "# File 2_1 contents",
3084                "file_2.py": "# File 2_2 contents",
3085                "file_3.py": "# File 2_3 contents",
3086            },
3087            "always_included_but_ignored_dir": {
3088                "file_a.py": "# File contents",
3089                "file_b.py": "# File contents",
3090                "file_c.py": "# File contents",
3091            },
3092        }),
3093    )
3094    .await;
3095
3096    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3097    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3098    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3099    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3100
3101    assert_eq!(
3102        visible_entries_as_strings(&panel, 0..20, cx),
3103        &[
3104            "v project_root",
3105            "    > .git",
3106            "    > always_included_but_ignored_dir",
3107            "    > dir_1",
3108            "    > dir_2",
3109            "      .gitignore",
3110        ]
3111    );
3112
3113    let gitignored_dir_file =
3114        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3115    let always_included_but_ignored_dir_file = find_project_entry(
3116        &panel,
3117        "project_root/always_included_but_ignored_dir/file_a.py",
3118        cx,
3119    )
3120    .expect("file that is .gitignored but set to always be included should have an entry");
3121    assert_eq!(
3122        gitignored_dir_file, None,
3123        "File in the gitignored dir should not have an entry unless its directory is toggled"
3124    );
3125
3126    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3127    cx.run_until_parked();
3128    cx.update(|_, cx| {
3129        cx.update_global::<SettingsStore, _>(|store, cx| {
3130            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3131                project_panel_settings.auto_reveal_entries = Some(true)
3132            });
3133        })
3134    });
3135
3136    panel.update(cx, |panel, cx| {
3137        panel.project.update(cx, |_, cx| {
3138            cx.emit(project::Event::ActiveEntryChanged(Some(
3139                always_included_but_ignored_dir_file,
3140            )))
3141        })
3142    });
3143    cx.run_until_parked();
3144
3145    assert_eq!(
3146        visible_entries_as_strings(&panel, 0..20, cx),
3147        &[
3148            "v project_root",
3149            "    > .git",
3150            "    v always_included_but_ignored_dir",
3151            "          file_a.py  <== selected  <== marked",
3152            "          file_b.py",
3153            "          file_c.py",
3154            "    v dir_1",
3155            "        > gitignored_dir",
3156            "          file_1.py",
3157            "          file_2.py",
3158            "          file_3.py",
3159            "    > dir_2",
3160            "      .gitignore",
3161        ],
3162        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3163    );
3164}
3165
3166#[gpui::test]
3167async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3168    init_test_with_editor(cx);
3169    cx.update(|cx| {
3170        cx.update_global::<SettingsStore, _>(|store, cx| {
3171            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3172                worktree_settings.file_scan_exclusions = Some(Vec::new());
3173            });
3174            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3175                project_panel_settings.auto_reveal_entries = Some(false)
3176            });
3177        })
3178    });
3179
3180    let fs = FakeFs::new(cx.background_executor.clone());
3181    fs.insert_tree(
3182        "/project_root",
3183        json!({
3184            ".git": {},
3185            ".gitignore": "**/gitignored_dir",
3186            "dir_1": {
3187                "file_1.py": "# File 1_1 contents",
3188                "file_2.py": "# File 1_2 contents",
3189                "file_3.py": "# File 1_3 contents",
3190                "gitignored_dir": {
3191                    "file_a.py": "# File contents",
3192                    "file_b.py": "# File contents",
3193                    "file_c.py": "# File contents",
3194                },
3195            },
3196            "dir_2": {
3197                "file_1.py": "# File 2_1 contents",
3198                "file_2.py": "# File 2_2 contents",
3199                "file_3.py": "# File 2_3 contents",
3200            }
3201        }),
3202    )
3203    .await;
3204
3205    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3206    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3207    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3208    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3209
3210    assert_eq!(
3211        visible_entries_as_strings(&panel, 0..20, cx),
3212        &[
3213            "v project_root",
3214            "    > .git",
3215            "    > dir_1",
3216            "    > dir_2",
3217            "      .gitignore",
3218        ]
3219    );
3220
3221    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3222        .expect("dir 1 file is not ignored and should have an entry");
3223    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3224        .expect("dir 2 file is not ignored and should have an entry");
3225    let gitignored_dir_file =
3226        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3227    assert_eq!(
3228        gitignored_dir_file, None,
3229        "File in the gitignored dir should not have an entry before its dir is toggled"
3230    );
3231
3232    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3233    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3234    cx.run_until_parked();
3235    assert_eq!(
3236        visible_entries_as_strings(&panel, 0..20, cx),
3237        &[
3238            "v project_root",
3239            "    > .git",
3240            "    v dir_1",
3241            "        v gitignored_dir  <== selected",
3242            "              file_a.py",
3243            "              file_b.py",
3244            "              file_c.py",
3245            "          file_1.py",
3246            "          file_2.py",
3247            "          file_3.py",
3248            "    > dir_2",
3249            "      .gitignore",
3250        ],
3251        "Should show gitignored dir file list in the project panel"
3252    );
3253    let gitignored_dir_file =
3254        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3255            .expect("after gitignored dir got opened, a file entry should be present");
3256
3257    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3258    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3259    assert_eq!(
3260        visible_entries_as_strings(&panel, 0..20, cx),
3261        &[
3262            "v project_root",
3263            "    > .git",
3264            "    > dir_1  <== selected",
3265            "    > dir_2",
3266            "      .gitignore",
3267        ],
3268        "Should hide all dir contents again and prepare for the explicit reveal test"
3269    );
3270
3271    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3272        panel.update(cx, |panel, cx| {
3273            panel.project.update(cx, |_, cx| {
3274                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3275            })
3276        });
3277        cx.run_until_parked();
3278        assert_eq!(
3279            visible_entries_as_strings(&panel, 0..20, cx),
3280            &[
3281                "v project_root",
3282                "    > .git",
3283                "    > dir_1  <== selected",
3284                "    > dir_2",
3285                "      .gitignore",
3286            ],
3287            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3288        );
3289    }
3290
3291    panel.update(cx, |panel, cx| {
3292        panel.project.update(cx, |_, cx| {
3293            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3294        })
3295    });
3296    cx.run_until_parked();
3297    assert_eq!(
3298        visible_entries_as_strings(&panel, 0..20, cx),
3299        &[
3300            "v project_root",
3301            "    > .git",
3302            "    v dir_1",
3303            "        > gitignored_dir",
3304            "          file_1.py  <== selected  <== marked",
3305            "          file_2.py",
3306            "          file_3.py",
3307            "    > dir_2",
3308            "      .gitignore",
3309        ],
3310        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3311    );
3312
3313    panel.update(cx, |panel, cx| {
3314        panel.project.update(cx, |_, cx| {
3315            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3316        })
3317    });
3318    cx.run_until_parked();
3319    assert_eq!(
3320        visible_entries_as_strings(&panel, 0..20, cx),
3321        &[
3322            "v project_root",
3323            "    > .git",
3324            "    v dir_1",
3325            "        > gitignored_dir",
3326            "          file_1.py",
3327            "          file_2.py",
3328            "          file_3.py",
3329            "    v dir_2",
3330            "          file_1.py  <== selected  <== marked",
3331            "          file_2.py",
3332            "          file_3.py",
3333            "      .gitignore",
3334        ],
3335        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3336    );
3337
3338    panel.update(cx, |panel, cx| {
3339        panel.project.update(cx, |_, cx| {
3340            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3341        })
3342    });
3343    cx.run_until_parked();
3344    assert_eq!(
3345        visible_entries_as_strings(&panel, 0..20, cx),
3346        &[
3347            "v project_root",
3348            "    > .git",
3349            "    v dir_1",
3350            "        v gitignored_dir",
3351            "              file_a.py  <== selected  <== marked",
3352            "              file_b.py",
3353            "              file_c.py",
3354            "          file_1.py",
3355            "          file_2.py",
3356            "          file_3.py",
3357            "    v dir_2",
3358            "          file_1.py",
3359            "          file_2.py",
3360            "          file_3.py",
3361            "      .gitignore",
3362        ],
3363        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3364    );
3365}
3366
3367#[gpui::test]
3368async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3369    init_test(cx);
3370    cx.update(|cx| {
3371        cx.update_global::<SettingsStore, _>(|store, cx| {
3372            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3373                project_settings.file_scan_exclusions =
3374                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3375            });
3376        });
3377    });
3378
3379    cx.update(|cx| {
3380        register_project_item::<TestProjectItemView>(cx);
3381    });
3382
3383    let fs = FakeFs::new(cx.executor().clone());
3384    fs.insert_tree(
3385        "/root1",
3386        json!({
3387            ".dockerignore": "",
3388            ".git": {
3389                "HEAD": "",
3390            },
3391        }),
3392    )
3393    .await;
3394
3395    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3396    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3397    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3398    let panel = workspace
3399        .update(cx, |workspace, window, cx| {
3400            let panel = ProjectPanel::new(workspace, window, cx);
3401            workspace.add_panel(panel.clone(), window, cx);
3402            panel
3403        })
3404        .unwrap();
3405
3406    select_path(&panel, "root1", cx);
3407    assert_eq!(
3408        visible_entries_as_strings(&panel, 0..10, cx),
3409        &["v root1  <== selected", "      .dockerignore",]
3410    );
3411    workspace
3412        .update(cx, |workspace, _, cx| {
3413            assert!(
3414                workspace.active_item(cx).is_none(),
3415                "Should have no active items in the beginning"
3416            );
3417        })
3418        .unwrap();
3419
3420    let excluded_file_path = ".git/COMMIT_EDITMSG";
3421    let excluded_dir_path = "excluded_dir";
3422
3423    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3424    panel.update_in(cx, |panel, window, cx| {
3425        assert!(panel.filename_editor.read(cx).is_focused(window));
3426    });
3427    panel
3428        .update_in(cx, |panel, window, cx| {
3429            panel.filename_editor.update(cx, |editor, cx| {
3430                editor.set_text(excluded_file_path, window, cx)
3431            });
3432            panel.confirm_edit(window, cx).unwrap()
3433        })
3434        .await
3435        .unwrap();
3436
3437    assert_eq!(
3438        visible_entries_as_strings(&panel, 0..13, cx),
3439        &["v root1", "      .dockerignore"],
3440        "Excluded dir should not be shown after opening a file in it"
3441    );
3442    panel.update_in(cx, |panel, window, cx| {
3443        assert!(
3444            !panel.filename_editor.read(cx).is_focused(window),
3445            "Should have closed the file name editor"
3446        );
3447    });
3448    workspace
3449        .update(cx, |workspace, _, cx| {
3450            let active_entry_path = workspace
3451                .active_item(cx)
3452                .expect("should have opened and activated the excluded item")
3453                .act_as::<TestProjectItemView>(cx)
3454                .expect("should have opened the corresponding project item for the excluded item")
3455                .read(cx)
3456                .path
3457                .clone();
3458            assert_eq!(
3459                active_entry_path.path.as_ref(),
3460                Path::new(excluded_file_path),
3461                "Should open the excluded file"
3462            );
3463
3464            assert!(
3465                workspace.notification_ids().is_empty(),
3466                "Should have no notifications after opening an excluded file"
3467            );
3468        })
3469        .unwrap();
3470    assert!(
3471        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3472        "Should have created the excluded file"
3473    );
3474
3475    select_path(&panel, "root1", cx);
3476    panel.update_in(cx, |panel, window, cx| {
3477        panel.new_directory(&NewDirectory, window, cx)
3478    });
3479    panel.update_in(cx, |panel, window, cx| {
3480        assert!(panel.filename_editor.read(cx).is_focused(window));
3481    });
3482    panel
3483        .update_in(cx, |panel, window, cx| {
3484            panel.filename_editor.update(cx, |editor, cx| {
3485                editor.set_text(excluded_file_path, window, cx)
3486            });
3487            panel.confirm_edit(window, cx).unwrap()
3488        })
3489        .await
3490        .unwrap();
3491
3492    assert_eq!(
3493        visible_entries_as_strings(&panel, 0..13, cx),
3494        &["v root1", "      .dockerignore"],
3495        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3496    );
3497    panel.update_in(cx, |panel, window, cx| {
3498        assert!(
3499            !panel.filename_editor.read(cx).is_focused(window),
3500            "Should have closed the file name editor"
3501        );
3502    });
3503    workspace
3504        .update(cx, |workspace, _, cx| {
3505            let notifications = workspace.notification_ids();
3506            assert_eq!(
3507                notifications.len(),
3508                1,
3509                "Should receive one notification with the error message"
3510            );
3511            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3512            assert!(workspace.notification_ids().is_empty());
3513        })
3514        .unwrap();
3515
3516    select_path(&panel, "root1", cx);
3517    panel.update_in(cx, |panel, window, cx| {
3518        panel.new_directory(&NewDirectory, window, cx)
3519    });
3520    panel.update_in(cx, |panel, window, cx| {
3521        assert!(panel.filename_editor.read(cx).is_focused(window));
3522    });
3523    panel
3524        .update_in(cx, |panel, window, cx| {
3525            panel.filename_editor.update(cx, |editor, cx| {
3526                editor.set_text(excluded_dir_path, window, cx)
3527            });
3528            panel.confirm_edit(window, cx).unwrap()
3529        })
3530        .await
3531        .unwrap();
3532
3533    assert_eq!(
3534        visible_entries_as_strings(&panel, 0..13, cx),
3535        &["v root1", "      .dockerignore"],
3536        "Should not change the project panel after trying to create an excluded directory"
3537    );
3538    panel.update_in(cx, |panel, window, cx| {
3539        assert!(
3540            !panel.filename_editor.read(cx).is_focused(window),
3541            "Should have closed the file name editor"
3542        );
3543    });
3544    workspace
3545        .update(cx, |workspace, _, cx| {
3546            let notifications = workspace.notification_ids();
3547            assert_eq!(
3548                notifications.len(),
3549                1,
3550                "Should receive one notification explaining that no directory is actually shown"
3551            );
3552            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3553            assert!(workspace.notification_ids().is_empty());
3554        })
3555        .unwrap();
3556    assert!(
3557        fs.is_dir(Path::new("/root1/excluded_dir")).await,
3558        "Should have created the excluded directory"
3559    );
3560}
3561
3562#[gpui::test]
3563async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3564    init_test_with_editor(cx);
3565
3566    let fs = FakeFs::new(cx.executor().clone());
3567    fs.insert_tree(
3568        "/src",
3569        json!({
3570            "test": {
3571                "first.rs": "// First Rust file",
3572                "second.rs": "// Second Rust file",
3573                "third.rs": "// Third Rust file",
3574            }
3575        }),
3576    )
3577    .await;
3578
3579    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3580    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3581    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3582    let panel = workspace
3583        .update(cx, |workspace, window, cx| {
3584            let panel = ProjectPanel::new(workspace, window, cx);
3585            workspace.add_panel(panel.clone(), window, cx);
3586            panel
3587        })
3588        .unwrap();
3589
3590    select_path(&panel, "src/", cx);
3591    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3592    cx.executor().run_until_parked();
3593    assert_eq!(
3594        visible_entries_as_strings(&panel, 0..10, cx),
3595        &[
3596            //
3597            "v src  <== selected",
3598            "    > test"
3599        ]
3600    );
3601    panel.update_in(cx, |panel, window, cx| {
3602        panel.new_directory(&NewDirectory, window, cx)
3603    });
3604    panel.update_in(cx, |panel, window, cx| {
3605        assert!(panel.filename_editor.read(cx).is_focused(window));
3606    });
3607    assert_eq!(
3608        visible_entries_as_strings(&panel, 0..10, cx),
3609        &[
3610            //
3611            "v src",
3612            "    > [EDITOR: '']  <== selected",
3613            "    > test"
3614        ]
3615    );
3616
3617    panel.update_in(cx, |panel, window, cx| {
3618        panel.cancel(&menu::Cancel, window, cx)
3619    });
3620    assert_eq!(
3621        visible_entries_as_strings(&panel, 0..10, cx),
3622        &[
3623            //
3624            "v src  <== selected",
3625            "    > test"
3626        ]
3627    );
3628}
3629
3630#[gpui::test]
3631async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3632    init_test_with_editor(cx);
3633
3634    let fs = FakeFs::new(cx.executor().clone());
3635    fs.insert_tree(
3636        "/root",
3637        json!({
3638            "dir1": {
3639                "subdir1": {},
3640                "file1.txt": "",
3641                "file2.txt": "",
3642            },
3643            "dir2": {
3644                "subdir2": {},
3645                "file3.txt": "",
3646                "file4.txt": "",
3647            },
3648            "file5.txt": "",
3649            "file6.txt": "",
3650        }),
3651    )
3652    .await;
3653
3654    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3655    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3656    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3657    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3658
3659    toggle_expand_dir(&panel, "root/dir1", cx);
3660    toggle_expand_dir(&panel, "root/dir2", cx);
3661
3662    // Test Case 1: Delete middle file in directory
3663    select_path(&panel, "root/dir1/file1.txt", cx);
3664    assert_eq!(
3665        visible_entries_as_strings(&panel, 0..15, cx),
3666        &[
3667            "v root",
3668            "    v dir1",
3669            "        > subdir1",
3670            "          file1.txt  <== selected",
3671            "          file2.txt",
3672            "    v dir2",
3673            "        > subdir2",
3674            "          file3.txt",
3675            "          file4.txt",
3676            "      file5.txt",
3677            "      file6.txt",
3678        ],
3679        "Initial state before deleting middle file"
3680    );
3681
3682    submit_deletion(&panel, cx);
3683    assert_eq!(
3684        visible_entries_as_strings(&panel, 0..15, cx),
3685        &[
3686            "v root",
3687            "    v dir1",
3688            "        > subdir1",
3689            "          file2.txt  <== selected",
3690            "    v dir2",
3691            "        > subdir2",
3692            "          file3.txt",
3693            "          file4.txt",
3694            "      file5.txt",
3695            "      file6.txt",
3696        ],
3697        "Should select next file after deleting middle file"
3698    );
3699
3700    // Test Case 2: Delete last file in directory
3701    submit_deletion(&panel, cx);
3702    assert_eq!(
3703        visible_entries_as_strings(&panel, 0..15, cx),
3704        &[
3705            "v root",
3706            "    v dir1",
3707            "        > subdir1  <== selected",
3708            "    v dir2",
3709            "        > subdir2",
3710            "          file3.txt",
3711            "          file4.txt",
3712            "      file5.txt",
3713            "      file6.txt",
3714        ],
3715        "Should select next directory when last file is deleted"
3716    );
3717
3718    // Test Case 3: Delete root level file
3719    select_path(&panel, "root/file6.txt", cx);
3720    assert_eq!(
3721        visible_entries_as_strings(&panel, 0..15, cx),
3722        &[
3723            "v root",
3724            "    v dir1",
3725            "        > subdir1",
3726            "    v dir2",
3727            "        > subdir2",
3728            "          file3.txt",
3729            "          file4.txt",
3730            "      file5.txt",
3731            "      file6.txt  <== selected",
3732        ],
3733        "Initial state before deleting root level file"
3734    );
3735
3736    submit_deletion(&panel, cx);
3737    assert_eq!(
3738        visible_entries_as_strings(&panel, 0..15, cx),
3739        &[
3740            "v root",
3741            "    v dir1",
3742            "        > subdir1",
3743            "    v dir2",
3744            "        > subdir2",
3745            "          file3.txt",
3746            "          file4.txt",
3747            "      file5.txt  <== selected",
3748        ],
3749        "Should select prev entry at root level"
3750    );
3751}
3752
3753#[gpui::test]
3754async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3755    init_test_with_editor(cx);
3756
3757    let fs = FakeFs::new(cx.executor().clone());
3758    fs.insert_tree(
3759        path!("/root"),
3760        json!({
3761            "aa": "// Testing 1",
3762            "bb": "// Testing 2",
3763            "cc": "// Testing 3",
3764            "dd": "// Testing 4",
3765            "ee": "// Testing 5",
3766            "ff": "// Testing 6",
3767            "gg": "// Testing 7",
3768            "hh": "// Testing 8",
3769            "ii": "// Testing 8",
3770            ".gitignore": "bb\ndd\nee\nff\nii\n'",
3771        }),
3772    )
3773    .await;
3774
3775    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3776    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3777    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3778
3779    // Test 1: Auto selection with one gitignored file next to the deleted file
3780    cx.update(|_, cx| {
3781        let settings = *ProjectPanelSettings::get_global(cx);
3782        ProjectPanelSettings::override_global(
3783            ProjectPanelSettings {
3784                hide_gitignore: true,
3785                ..settings
3786            },
3787            cx,
3788        );
3789    });
3790
3791    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3792
3793    select_path(&panel, "root/aa", cx);
3794    assert_eq!(
3795        visible_entries_as_strings(&panel, 0..10, cx),
3796        &[
3797            "v root",
3798            "      .gitignore",
3799            "      aa  <== selected",
3800            "      cc",
3801            "      gg",
3802            "      hh"
3803        ],
3804        "Initial state should hide files on .gitignore"
3805    );
3806
3807    submit_deletion(&panel, cx);
3808
3809    assert_eq!(
3810        visible_entries_as_strings(&panel, 0..10, cx),
3811        &[
3812            "v root",
3813            "      .gitignore",
3814            "      cc  <== selected",
3815            "      gg",
3816            "      hh"
3817        ],
3818        "Should select next entry not on .gitignore"
3819    );
3820
3821    // Test 2: Auto selection with many gitignored files next to the deleted file
3822    submit_deletion(&panel, cx);
3823    assert_eq!(
3824        visible_entries_as_strings(&panel, 0..10, cx),
3825        &[
3826            "v root",
3827            "      .gitignore",
3828            "      gg  <== selected",
3829            "      hh"
3830        ],
3831        "Should select next entry not on .gitignore"
3832    );
3833
3834    // Test 3: Auto selection of entry before deleted file
3835    select_path(&panel, "root/hh", cx);
3836    assert_eq!(
3837        visible_entries_as_strings(&panel, 0..10, cx),
3838        &[
3839            "v root",
3840            "      .gitignore",
3841            "      gg",
3842            "      hh  <== selected"
3843        ],
3844        "Should select next entry not on .gitignore"
3845    );
3846    submit_deletion(&panel, cx);
3847    assert_eq!(
3848        visible_entries_as_strings(&panel, 0..10, cx),
3849        &["v root", "      .gitignore", "      gg  <== selected"],
3850        "Should select next entry not on .gitignore"
3851    );
3852}
3853
3854#[gpui::test]
3855async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
3856    init_test_with_editor(cx);
3857
3858    let fs = FakeFs::new(cx.executor().clone());
3859    fs.insert_tree(
3860        path!("/root"),
3861        json!({
3862            "dir1": {
3863                "file1": "// Testing",
3864                "file2": "// Testing",
3865                "file3": "// Testing"
3866            },
3867            "aa": "// Testing",
3868            ".gitignore": "file1\nfile3\n",
3869        }),
3870    )
3871    .await;
3872
3873    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3874    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3875    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3876
3877    cx.update(|_, cx| {
3878        let settings = *ProjectPanelSettings::get_global(cx);
3879        ProjectPanelSettings::override_global(
3880            ProjectPanelSettings {
3881                hide_gitignore: true,
3882                ..settings
3883            },
3884            cx,
3885        );
3886    });
3887
3888    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3889
3890    // Test 1: Visible items should exclude files on gitignore
3891    toggle_expand_dir(&panel, "root/dir1", cx);
3892    select_path(&panel, "root/dir1/file2", cx);
3893    assert_eq!(
3894        visible_entries_as_strings(&panel, 0..10, cx),
3895        &[
3896            "v root",
3897            "    v dir1",
3898            "          file2  <== selected",
3899            "      .gitignore",
3900            "      aa"
3901        ],
3902        "Initial state should hide files on .gitignore"
3903    );
3904    submit_deletion(&panel, cx);
3905
3906    // Test 2: Auto selection should go to the parent
3907    assert_eq!(
3908        visible_entries_as_strings(&panel, 0..10, cx),
3909        &[
3910            "v root",
3911            "    v dir1  <== selected",
3912            "      .gitignore",
3913            "      aa"
3914        ],
3915        "Initial state should hide files on .gitignore"
3916    );
3917}
3918
3919#[gpui::test]
3920async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
3921    init_test_with_editor(cx);
3922
3923    let fs = FakeFs::new(cx.executor().clone());
3924    fs.insert_tree(
3925        "/root",
3926        json!({
3927            "dir1": {
3928                "subdir1": {
3929                    "a.txt": "",
3930                    "b.txt": ""
3931                },
3932                "file1.txt": "",
3933            },
3934            "dir2": {
3935                "subdir2": {
3936                    "c.txt": "",
3937                    "d.txt": ""
3938                },
3939                "file2.txt": "",
3940            },
3941            "file3.txt": "",
3942        }),
3943    )
3944    .await;
3945
3946    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3947    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3948    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3949    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3950
3951    toggle_expand_dir(&panel, "root/dir1", cx);
3952    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
3953    toggle_expand_dir(&panel, "root/dir2", cx);
3954    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
3955
3956    // Test Case 1: Select and delete nested directory with parent
3957    cx.simulate_modifiers_change(gpui::Modifiers {
3958        control: true,
3959        ..Default::default()
3960    });
3961    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
3962    select_path_with_mark(&panel, "root/dir1", cx);
3963
3964    assert_eq!(
3965        visible_entries_as_strings(&panel, 0..15, cx),
3966        &[
3967            "v root",
3968            "    v dir1  <== selected  <== marked",
3969            "        v subdir1  <== marked",
3970            "              a.txt",
3971            "              b.txt",
3972            "          file1.txt",
3973            "    v dir2",
3974            "        v subdir2",
3975            "              c.txt",
3976            "              d.txt",
3977            "          file2.txt",
3978            "      file3.txt",
3979        ],
3980        "Initial state before deleting nested directory with parent"
3981    );
3982
3983    submit_deletion(&panel, cx);
3984    assert_eq!(
3985        visible_entries_as_strings(&panel, 0..15, cx),
3986        &[
3987            "v root",
3988            "    v dir2  <== selected",
3989            "        v subdir2",
3990            "              c.txt",
3991            "              d.txt",
3992            "          file2.txt",
3993            "      file3.txt",
3994        ],
3995        "Should select next directory after deleting directory with parent"
3996    );
3997
3998    // Test Case 2: Select mixed files and directories across levels
3999    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4000    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4001    select_path_with_mark(&panel, "root/file3.txt", cx);
4002
4003    assert_eq!(
4004        visible_entries_as_strings(&panel, 0..15, cx),
4005        &[
4006            "v root",
4007            "    v dir2",
4008            "        v subdir2",
4009            "              c.txt  <== marked",
4010            "              d.txt",
4011            "          file2.txt  <== marked",
4012            "      file3.txt  <== selected  <== marked",
4013        ],
4014        "Initial state before deleting"
4015    );
4016
4017    submit_deletion(&panel, cx);
4018    assert_eq!(
4019        visible_entries_as_strings(&panel, 0..15, cx),
4020        &[
4021            "v root",
4022            "    v dir2  <== selected",
4023            "        v subdir2",
4024            "              d.txt",
4025        ],
4026        "Should select sibling directory"
4027    );
4028}
4029
4030#[gpui::test]
4031async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4032    init_test_with_editor(cx);
4033
4034    let fs = FakeFs::new(cx.executor().clone());
4035    fs.insert_tree(
4036        "/root",
4037        json!({
4038            "dir1": {
4039                "subdir1": {
4040                    "a.txt": "",
4041                    "b.txt": ""
4042                },
4043                "file1.txt": "",
4044            },
4045            "dir2": {
4046                "subdir2": {
4047                    "c.txt": "",
4048                    "d.txt": ""
4049                },
4050                "file2.txt": "",
4051            },
4052            "file3.txt": "",
4053            "file4.txt": "",
4054        }),
4055    )
4056    .await;
4057
4058    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4059    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4060    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4061    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4062
4063    toggle_expand_dir(&panel, "root/dir1", cx);
4064    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4065    toggle_expand_dir(&panel, "root/dir2", cx);
4066    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4067
4068    // Test Case 1: Select all root files and directories
4069    cx.simulate_modifiers_change(gpui::Modifiers {
4070        control: true,
4071        ..Default::default()
4072    });
4073    select_path_with_mark(&panel, "root/dir1", cx);
4074    select_path_with_mark(&panel, "root/dir2", cx);
4075    select_path_with_mark(&panel, "root/file3.txt", cx);
4076    select_path_with_mark(&panel, "root/file4.txt", cx);
4077    assert_eq!(
4078        visible_entries_as_strings(&panel, 0..20, cx),
4079        &[
4080            "v root",
4081            "    v dir1  <== marked",
4082            "        v subdir1",
4083            "              a.txt",
4084            "              b.txt",
4085            "          file1.txt",
4086            "    v dir2  <== marked",
4087            "        v subdir2",
4088            "              c.txt",
4089            "              d.txt",
4090            "          file2.txt",
4091            "      file3.txt  <== marked",
4092            "      file4.txt  <== selected  <== marked",
4093        ],
4094        "State before deleting all contents"
4095    );
4096
4097    submit_deletion(&panel, cx);
4098    assert_eq!(
4099        visible_entries_as_strings(&panel, 0..20, cx),
4100        &["v root  <== selected"],
4101        "Only empty root directory should remain after deleting all contents"
4102    );
4103}
4104
4105#[gpui::test]
4106async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4107    init_test_with_editor(cx);
4108
4109    let fs = FakeFs::new(cx.executor().clone());
4110    fs.insert_tree(
4111        "/root",
4112        json!({
4113            "dir1": {
4114                "subdir1": {
4115                    "file_a.txt": "content a",
4116                    "file_b.txt": "content b",
4117                },
4118                "subdir2": {
4119                    "file_c.txt": "content c",
4120                },
4121                "file1.txt": "content 1",
4122            },
4123            "dir2": {
4124                "file2.txt": "content 2",
4125            },
4126        }),
4127    )
4128    .await;
4129
4130    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4131    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4132    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4133    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4134
4135    toggle_expand_dir(&panel, "root/dir1", cx);
4136    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4137    toggle_expand_dir(&panel, "root/dir2", cx);
4138    cx.simulate_modifiers_change(gpui::Modifiers {
4139        control: true,
4140        ..Default::default()
4141    });
4142
4143    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4144    select_path_with_mark(&panel, "root/dir1", cx);
4145    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4146    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4147
4148    assert_eq!(
4149        visible_entries_as_strings(&panel, 0..20, cx),
4150        &[
4151            "v root",
4152            "    v dir1  <== marked",
4153            "        v subdir1  <== marked",
4154            "              file_a.txt  <== selected  <== marked",
4155            "              file_b.txt",
4156            "        > subdir2",
4157            "          file1.txt",
4158            "    v dir2",
4159            "          file2.txt",
4160        ],
4161        "State with parent dir, subdir, and file selected"
4162    );
4163    submit_deletion(&panel, cx);
4164    assert_eq!(
4165        visible_entries_as_strings(&panel, 0..20, cx),
4166        &["v root", "    v dir2  <== selected", "          file2.txt",],
4167        "Only dir2 should remain after deletion"
4168    );
4169}
4170
4171#[gpui::test]
4172async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4173    init_test_with_editor(cx);
4174
4175    let fs = FakeFs::new(cx.executor().clone());
4176    // First worktree
4177    fs.insert_tree(
4178        "/root1",
4179        json!({
4180            "dir1": {
4181                "file1.txt": "content 1",
4182                "file2.txt": "content 2",
4183            },
4184            "dir2": {
4185                "file3.txt": "content 3",
4186            },
4187        }),
4188    )
4189    .await;
4190
4191    // Second worktree
4192    fs.insert_tree(
4193        "/root2",
4194        json!({
4195            "dir3": {
4196                "file4.txt": "content 4",
4197                "file5.txt": "content 5",
4198            },
4199            "file6.txt": "content 6",
4200        }),
4201    )
4202    .await;
4203
4204    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4205    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4206    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4207    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4208
4209    // Expand all directories for testing
4210    toggle_expand_dir(&panel, "root1/dir1", cx);
4211    toggle_expand_dir(&panel, "root1/dir2", cx);
4212    toggle_expand_dir(&panel, "root2/dir3", cx);
4213
4214    // Test Case 1: Delete files across different worktrees
4215    cx.simulate_modifiers_change(gpui::Modifiers {
4216        control: true,
4217        ..Default::default()
4218    });
4219    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4220    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4221
4222    assert_eq!(
4223        visible_entries_as_strings(&panel, 0..20, cx),
4224        &[
4225            "v root1",
4226            "    v dir1",
4227            "          file1.txt  <== marked",
4228            "          file2.txt",
4229            "    v dir2",
4230            "          file3.txt",
4231            "v root2",
4232            "    v dir3",
4233            "          file4.txt  <== selected  <== marked",
4234            "          file5.txt",
4235            "      file6.txt",
4236        ],
4237        "Initial state with files selected from different worktrees"
4238    );
4239
4240    submit_deletion(&panel, cx);
4241    assert_eq!(
4242        visible_entries_as_strings(&panel, 0..20, cx),
4243        &[
4244            "v root1",
4245            "    v dir1",
4246            "          file2.txt",
4247            "    v dir2",
4248            "          file3.txt",
4249            "v root2",
4250            "    v dir3",
4251            "          file5.txt  <== selected",
4252            "      file6.txt",
4253        ],
4254        "Should select next file in the last worktree after deletion"
4255    );
4256
4257    // Test Case 2: Delete directories from different worktrees
4258    select_path_with_mark(&panel, "root1/dir1", cx);
4259    select_path_with_mark(&panel, "root2/dir3", cx);
4260
4261    assert_eq!(
4262        visible_entries_as_strings(&panel, 0..20, cx),
4263        &[
4264            "v root1",
4265            "    v dir1  <== marked",
4266            "          file2.txt",
4267            "    v dir2",
4268            "          file3.txt",
4269            "v root2",
4270            "    v dir3  <== selected  <== marked",
4271            "          file5.txt",
4272            "      file6.txt",
4273        ],
4274        "State with directories marked from different worktrees"
4275    );
4276
4277    submit_deletion(&panel, cx);
4278    assert_eq!(
4279        visible_entries_as_strings(&panel, 0..20, cx),
4280        &[
4281            "v root1",
4282            "    v dir2",
4283            "          file3.txt",
4284            "v root2",
4285            "      file6.txt  <== selected",
4286        ],
4287        "Should select remaining file in last worktree after directory deletion"
4288    );
4289
4290    // Test Case 4: Delete all remaining files except roots
4291    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4292    select_path_with_mark(&panel, "root2/file6.txt", cx);
4293
4294    assert_eq!(
4295        visible_entries_as_strings(&panel, 0..20, cx),
4296        &[
4297            "v root1",
4298            "    v dir2",
4299            "          file3.txt  <== marked",
4300            "v root2",
4301            "      file6.txt  <== selected  <== marked",
4302        ],
4303        "State with all remaining files marked"
4304    );
4305
4306    submit_deletion(&panel, cx);
4307    assert_eq!(
4308        visible_entries_as_strings(&panel, 0..20, cx),
4309        &["v root1", "    v dir2", "v root2  <== selected"],
4310        "Second parent root should be selected after deleting"
4311    );
4312}
4313
4314#[gpui::test]
4315async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4316    init_test_with_editor(cx);
4317
4318    let fs = FakeFs::new(cx.executor().clone());
4319    fs.insert_tree(
4320        "/root",
4321        json!({
4322            "dir1": {
4323                "file1.txt": "",
4324                "file2.txt": "",
4325                "file3.txt": "",
4326            },
4327            "dir2": {
4328                "file4.txt": "",
4329                "file5.txt": "",
4330            },
4331        }),
4332    )
4333    .await;
4334
4335    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4336    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4337    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4338    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4339
4340    toggle_expand_dir(&panel, "root/dir1", cx);
4341    toggle_expand_dir(&panel, "root/dir2", cx);
4342
4343    cx.simulate_modifiers_change(gpui::Modifiers {
4344        control: true,
4345        ..Default::default()
4346    });
4347
4348    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4349    select_path(&panel, "root/dir1/file1.txt", cx);
4350
4351    assert_eq!(
4352        visible_entries_as_strings(&panel, 0..15, cx),
4353        &[
4354            "v root",
4355            "    v dir1",
4356            "          file1.txt  <== selected",
4357            "          file2.txt  <== marked",
4358            "          file3.txt",
4359            "    v dir2",
4360            "          file4.txt",
4361            "          file5.txt",
4362        ],
4363        "Initial state with one marked entry and different selection"
4364    );
4365
4366    // Delete should operate on the selected entry (file1.txt)
4367    submit_deletion(&panel, cx);
4368    assert_eq!(
4369        visible_entries_as_strings(&panel, 0..15, cx),
4370        &[
4371            "v root",
4372            "    v dir1",
4373            "          file2.txt  <== selected  <== marked",
4374            "          file3.txt",
4375            "    v dir2",
4376            "          file4.txt",
4377            "          file5.txt",
4378        ],
4379        "Should delete selected file, not marked file"
4380    );
4381
4382    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4383    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4384    select_path(&panel, "root/dir2/file5.txt", cx);
4385
4386    assert_eq!(
4387        visible_entries_as_strings(&panel, 0..15, cx),
4388        &[
4389            "v root",
4390            "    v dir1",
4391            "          file2.txt  <== marked",
4392            "          file3.txt  <== marked",
4393            "    v dir2",
4394            "          file4.txt  <== marked",
4395            "          file5.txt  <== selected",
4396        ],
4397        "Initial state with multiple marked entries and different selection"
4398    );
4399
4400    // Delete should operate on all marked entries, ignoring the selection
4401    submit_deletion(&panel, cx);
4402    assert_eq!(
4403        visible_entries_as_strings(&panel, 0..15, cx),
4404        &[
4405            "v root",
4406            "    v dir1",
4407            "    v dir2",
4408            "          file5.txt  <== selected",
4409        ],
4410        "Should delete all marked files, leaving only the selected file"
4411    );
4412}
4413
4414#[gpui::test]
4415async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4416    init_test_with_editor(cx);
4417
4418    let fs = FakeFs::new(cx.executor().clone());
4419    fs.insert_tree(
4420        "/root_b",
4421        json!({
4422            "dir1": {
4423                "file1.txt": "content 1",
4424                "file2.txt": "content 2",
4425            },
4426        }),
4427    )
4428    .await;
4429
4430    fs.insert_tree(
4431        "/root_c",
4432        json!({
4433            "dir2": {},
4434        }),
4435    )
4436    .await;
4437
4438    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4439    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4440    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4441    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4442
4443    toggle_expand_dir(&panel, "root_b/dir1", cx);
4444    toggle_expand_dir(&panel, "root_c/dir2", cx);
4445
4446    cx.simulate_modifiers_change(gpui::Modifiers {
4447        control: true,
4448        ..Default::default()
4449    });
4450    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4451    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4452
4453    assert_eq!(
4454        visible_entries_as_strings(&panel, 0..20, cx),
4455        &[
4456            "v root_b",
4457            "    v dir1",
4458            "          file1.txt  <== marked",
4459            "          file2.txt  <== selected  <== marked",
4460            "v root_c",
4461            "    v dir2",
4462        ],
4463        "Initial state with files marked in root_b"
4464    );
4465
4466    submit_deletion(&panel, cx);
4467    assert_eq!(
4468        visible_entries_as_strings(&panel, 0..20, cx),
4469        &[
4470            "v root_b",
4471            "    v dir1  <== selected",
4472            "v root_c",
4473            "    v dir2",
4474        ],
4475        "After deletion in root_b as it's last deletion, selection should be in root_b"
4476    );
4477
4478    select_path_with_mark(&panel, "root_c/dir2", cx);
4479
4480    submit_deletion(&panel, cx);
4481    assert_eq!(
4482        visible_entries_as_strings(&panel, 0..20, cx),
4483        &["v root_b", "    v dir1", "v root_c  <== selected",],
4484        "After deleting from root_c, it should remain in root_c"
4485    );
4486}
4487
4488fn toggle_expand_dir(
4489    panel: &Entity<ProjectPanel>,
4490    path: impl AsRef<Path>,
4491    cx: &mut VisualTestContext,
4492) {
4493    let path = path.as_ref();
4494    panel.update_in(cx, |panel, window, cx| {
4495        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4496            let worktree = worktree.read(cx);
4497            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4498                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4499                panel.toggle_expanded(entry_id, window, cx);
4500                return;
4501            }
4502        }
4503        panic!("no worktree for path {:?}", path);
4504    });
4505}
4506
4507#[gpui::test]
4508async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4509    init_test_with_editor(cx);
4510
4511    let fs = FakeFs::new(cx.executor().clone());
4512    fs.insert_tree(
4513        path!("/root"),
4514        json!({
4515            ".gitignore": "**/ignored_dir\n**/ignored_nested",
4516            "dir1": {
4517                "empty1": {
4518                    "empty2": {
4519                        "empty3": {
4520                            "file.txt": ""
4521                        }
4522                    }
4523                },
4524                "subdir1": {
4525                    "file1.txt": "",
4526                    "file2.txt": "",
4527                    "ignored_nested": {
4528                        "ignored_file.txt": ""
4529                    }
4530                },
4531                "ignored_dir": {
4532                    "subdir": {
4533                        "deep_file.txt": ""
4534                    }
4535                }
4536            }
4537        }),
4538    )
4539    .await;
4540
4541    let project = Project::test(fs.clone(), [path!("/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
4545    // Test 1: When auto-fold is enabled
4546    cx.update(|_, cx| {
4547        let settings = *ProjectPanelSettings::get_global(cx);
4548        ProjectPanelSettings::override_global(
4549            ProjectPanelSettings {
4550                auto_fold_dirs: true,
4551                ..settings
4552            },
4553            cx,
4554        );
4555    });
4556
4557    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4558
4559    assert_eq!(
4560        visible_entries_as_strings(&panel, 0..20, cx),
4561        &["v root", "    > dir1", "      .gitignore",],
4562        "Initial state should show collapsed root structure"
4563    );
4564
4565    toggle_expand_dir(&panel, "root/dir1", cx);
4566    assert_eq!(
4567        visible_entries_as_strings(&panel, 0..20, cx),
4568        &[
4569            separator!("v root"),
4570            separator!("    v dir1  <== selected"),
4571            separator!("        > empty1/empty2/empty3"),
4572            separator!("        > ignored_dir"),
4573            separator!("        > subdir1"),
4574            separator!("      .gitignore"),
4575        ],
4576        "Should show first level with auto-folded dirs and ignored dir visible"
4577    );
4578
4579    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4580    panel.update(cx, |panel, cx| {
4581        let project = panel.project.read(cx);
4582        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4583        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4584        panel.update_visible_entries(None, cx);
4585    });
4586    cx.run_until_parked();
4587
4588    assert_eq!(
4589        visible_entries_as_strings(&panel, 0..20, cx),
4590        &[
4591            separator!("v root"),
4592            separator!("    v dir1  <== selected"),
4593            separator!("        v empty1"),
4594            separator!("            v empty2"),
4595            separator!("                v empty3"),
4596            separator!("                      file.txt"),
4597            separator!("        > ignored_dir"),
4598            separator!("        v subdir1"),
4599            separator!("            > ignored_nested"),
4600            separator!("              file1.txt"),
4601            separator!("              file2.txt"),
4602            separator!("      .gitignore"),
4603        ],
4604        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4605    );
4606
4607    // Test 2: When auto-fold is disabled
4608    cx.update(|_, cx| {
4609        let settings = *ProjectPanelSettings::get_global(cx);
4610        ProjectPanelSettings::override_global(
4611            ProjectPanelSettings {
4612                auto_fold_dirs: false,
4613                ..settings
4614            },
4615            cx,
4616        );
4617    });
4618
4619    panel.update_in(cx, |panel, window, cx| {
4620        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4621    });
4622
4623    toggle_expand_dir(&panel, "root/dir1", cx);
4624    assert_eq!(
4625        visible_entries_as_strings(&panel, 0..20, cx),
4626        &[
4627            separator!("v root"),
4628            separator!("    v dir1  <== selected"),
4629            separator!("        > empty1"),
4630            separator!("        > ignored_dir"),
4631            separator!("        > subdir1"),
4632            separator!("      .gitignore"),
4633        ],
4634        "With auto-fold disabled: should show all directories separately"
4635    );
4636
4637    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4638    panel.update(cx, |panel, cx| {
4639        let project = panel.project.read(cx);
4640        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4641        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4642        panel.update_visible_entries(None, cx);
4643    });
4644    cx.run_until_parked();
4645
4646    assert_eq!(
4647        visible_entries_as_strings(&panel, 0..20, cx),
4648        &[
4649            separator!("v root"),
4650            separator!("    v dir1  <== selected"),
4651            separator!("        v empty1"),
4652            separator!("            v empty2"),
4653            separator!("                v empty3"),
4654            separator!("                      file.txt"),
4655            separator!("        > ignored_dir"),
4656            separator!("        v subdir1"),
4657            separator!("            > ignored_nested"),
4658            separator!("              file1.txt"),
4659            separator!("              file2.txt"),
4660            separator!("      .gitignore"),
4661        ],
4662        "After expand_all without auto-fold: should expand all dirs normally, \
4663         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4664    );
4665
4666    // Test 3: When explicitly called on ignored directory
4667    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4668    panel.update(cx, |panel, cx| {
4669        let project = panel.project.read(cx);
4670        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4671        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4672        panel.update_visible_entries(None, cx);
4673    });
4674    cx.run_until_parked();
4675
4676    assert_eq!(
4677        visible_entries_as_strings(&panel, 0..20, cx),
4678        &[
4679            separator!("v root"),
4680            separator!("    v dir1  <== selected"),
4681            separator!("        v empty1"),
4682            separator!("            v empty2"),
4683            separator!("                v empty3"),
4684            separator!("                      file.txt"),
4685            separator!("        v ignored_dir"),
4686            separator!("            v subdir"),
4687            separator!("                  deep_file.txt"),
4688            separator!("        v subdir1"),
4689            separator!("            > ignored_nested"),
4690            separator!("              file1.txt"),
4691            separator!("              file2.txt"),
4692            separator!("      .gitignore"),
4693        ],
4694        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4695    );
4696}
4697
4698#[gpui::test]
4699async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4700    init_test(cx);
4701
4702    let fs = FakeFs::new(cx.executor().clone());
4703    fs.insert_tree(
4704        path!("/root"),
4705        json!({
4706            "dir1": {
4707                "subdir1": {
4708                    "nested1": {
4709                        "file1.txt": "",
4710                        "file2.txt": ""
4711                    },
4712                },
4713                "subdir2": {
4714                    "file4.txt": ""
4715                }
4716            },
4717            "dir2": {
4718                "single_file": {
4719                    "file5.txt": ""
4720                }
4721            }
4722        }),
4723    )
4724    .await;
4725
4726    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4727    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4728    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4729
4730    // Test 1: Basic collapsing
4731    {
4732        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4733
4734        toggle_expand_dir(&panel, "root/dir1", cx);
4735        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4736        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4737        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4738
4739        assert_eq!(
4740            visible_entries_as_strings(&panel, 0..20, cx),
4741            &[
4742                separator!("v root"),
4743                separator!("    v dir1"),
4744                separator!("        v subdir1"),
4745                separator!("            v nested1"),
4746                separator!("                  file1.txt"),
4747                separator!("                  file2.txt"),
4748                separator!("        v subdir2  <== selected"),
4749                separator!("              file4.txt"),
4750                separator!("    > dir2"),
4751            ],
4752            "Initial state with everything expanded"
4753        );
4754
4755        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4756        panel.update(cx, |panel, cx| {
4757            let project = panel.project.read(cx);
4758            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4759            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4760            panel.update_visible_entries(None, cx);
4761        });
4762
4763        assert_eq!(
4764            visible_entries_as_strings(&panel, 0..20, cx),
4765            &["v root", "    > dir1", "    > dir2",],
4766            "All subdirs under dir1 should be collapsed"
4767        );
4768    }
4769
4770    // Test 2: With auto-fold enabled
4771    {
4772        cx.update(|_, cx| {
4773            let settings = *ProjectPanelSettings::get_global(cx);
4774            ProjectPanelSettings::override_global(
4775                ProjectPanelSettings {
4776                    auto_fold_dirs: true,
4777                    ..settings
4778                },
4779                cx,
4780            );
4781        });
4782
4783        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4784
4785        toggle_expand_dir(&panel, "root/dir1", cx);
4786        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4787        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4788
4789        assert_eq!(
4790            visible_entries_as_strings(&panel, 0..20, cx),
4791            &[
4792                separator!("v root"),
4793                separator!("    v dir1"),
4794                separator!("        v subdir1/nested1  <== selected"),
4795                separator!("              file1.txt"),
4796                separator!("              file2.txt"),
4797                separator!("        > subdir2"),
4798                separator!("    > dir2/single_file"),
4799            ],
4800            "Initial state with some dirs expanded"
4801        );
4802
4803        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4804        panel.update(cx, |panel, cx| {
4805            let project = panel.project.read(cx);
4806            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4807            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4808        });
4809
4810        toggle_expand_dir(&panel, "root/dir1", cx);
4811
4812        assert_eq!(
4813            visible_entries_as_strings(&panel, 0..20, cx),
4814            &[
4815                separator!("v root"),
4816                separator!("    v dir1  <== selected"),
4817                separator!("        > subdir1/nested1"),
4818                separator!("        > subdir2"),
4819                separator!("    > dir2/single_file"),
4820            ],
4821            "Subdirs should be collapsed and folded with auto-fold enabled"
4822        );
4823    }
4824
4825    // Test 3: With auto-fold disabled
4826    {
4827        cx.update(|_, cx| {
4828            let settings = *ProjectPanelSettings::get_global(cx);
4829            ProjectPanelSettings::override_global(
4830                ProjectPanelSettings {
4831                    auto_fold_dirs: false,
4832                    ..settings
4833                },
4834                cx,
4835            );
4836        });
4837
4838        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4839
4840        toggle_expand_dir(&panel, "root/dir1", cx);
4841        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4842        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4843
4844        assert_eq!(
4845            visible_entries_as_strings(&panel, 0..20, cx),
4846            &[
4847                separator!("v root"),
4848                separator!("    v dir1"),
4849                separator!("        v subdir1"),
4850                separator!("            v nested1  <== selected"),
4851                separator!("                  file1.txt"),
4852                separator!("                  file2.txt"),
4853                separator!("        > subdir2"),
4854                separator!("    > dir2"),
4855            ],
4856            "Initial state with some dirs expanded and auto-fold disabled"
4857        );
4858
4859        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4860        panel.update(cx, |panel, cx| {
4861            let project = panel.project.read(cx);
4862            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4863            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4864        });
4865
4866        toggle_expand_dir(&panel, "root/dir1", cx);
4867
4868        assert_eq!(
4869            visible_entries_as_strings(&panel, 0..20, cx),
4870            &[
4871                separator!("v root"),
4872                separator!("    v dir1  <== selected"),
4873                separator!("        > subdir1"),
4874                separator!("        > subdir2"),
4875                separator!("    > dir2"),
4876            ],
4877            "Subdirs should be collapsed but not folded with auto-fold disabled"
4878        );
4879    }
4880}
4881
4882fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
4883    let path = path.as_ref();
4884    panel.update(cx, |panel, cx| {
4885        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4886            let worktree = worktree.read(cx);
4887            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4888                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4889                panel.selection = Some(crate::SelectedEntry {
4890                    worktree_id: worktree.id(),
4891                    entry_id,
4892                });
4893                return;
4894            }
4895        }
4896        panic!("no worktree for path {:?}", path);
4897    });
4898}
4899
4900fn select_path_with_mark(
4901    panel: &Entity<ProjectPanel>,
4902    path: impl AsRef<Path>,
4903    cx: &mut VisualTestContext,
4904) {
4905    let path = path.as_ref();
4906    panel.update(cx, |panel, cx| {
4907        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4908            let worktree = worktree.read(cx);
4909            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4910                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4911                let entry = crate::SelectedEntry {
4912                    worktree_id: worktree.id(),
4913                    entry_id,
4914                };
4915                if !panel.marked_entries.contains(&entry) {
4916                    panel.marked_entries.insert(entry);
4917                }
4918                panel.selection = Some(entry);
4919                return;
4920            }
4921        }
4922        panic!("no worktree for path {:?}", path);
4923    });
4924}
4925
4926fn find_project_entry(
4927    panel: &Entity<ProjectPanel>,
4928    path: impl AsRef<Path>,
4929    cx: &mut VisualTestContext,
4930) -> Option<ProjectEntryId> {
4931    let path = path.as_ref();
4932    panel.update(cx, |panel, cx| {
4933        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4934            let worktree = worktree.read(cx);
4935            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4936                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
4937            }
4938        }
4939        panic!("no worktree for path {path:?}");
4940    })
4941}
4942
4943fn visible_entries_as_strings(
4944    panel: &Entity<ProjectPanel>,
4945    range: Range<usize>,
4946    cx: &mut VisualTestContext,
4947) -> Vec<String> {
4948    let mut result = Vec::new();
4949    let mut project_entries = HashSet::default();
4950    let mut has_editor = false;
4951
4952    panel.update_in(cx, |panel, window, cx| {
4953        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
4954            if details.is_editing {
4955                assert!(!has_editor, "duplicate editor entry");
4956                has_editor = true;
4957            } else {
4958                assert!(
4959                    project_entries.insert(project_entry),
4960                    "duplicate project entry {:?} {:?}",
4961                    project_entry,
4962                    details
4963                );
4964            }
4965
4966            let indent = "    ".repeat(details.depth);
4967            let icon = if details.kind.is_dir() {
4968                if details.is_expanded { "v " } else { "> " }
4969            } else {
4970                "  "
4971            };
4972            let name = if details.is_editing {
4973                format!("[EDITOR: '{}']", details.filename)
4974            } else if details.is_processing {
4975                format!("[PROCESSING: '{}']", details.filename)
4976            } else {
4977                details.filename.clone()
4978            };
4979            let selected = if details.is_selected {
4980                "  <== selected"
4981            } else {
4982                ""
4983            };
4984            let marked = if details.is_marked {
4985                "  <== marked"
4986            } else {
4987                ""
4988            };
4989
4990            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
4991        });
4992    });
4993
4994    result
4995}
4996
4997fn init_test(cx: &mut TestAppContext) {
4998    cx.update(|cx| {
4999        let settings_store = SettingsStore::test(cx);
5000        cx.set_global(settings_store);
5001        init_settings(cx);
5002        theme::init(theme::LoadThemes::JustBase, cx);
5003        language::init(cx);
5004        editor::init_settings(cx);
5005        crate::init(cx);
5006        workspace::init_settings(cx);
5007        client::init_settings(cx);
5008        Project::init_settings(cx);
5009
5010        cx.update_global::<SettingsStore, _>(|store, cx| {
5011            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5012                project_panel_settings.auto_fold_dirs = Some(false);
5013            });
5014            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5015                worktree_settings.file_scan_exclusions = Some(Vec::new());
5016            });
5017        });
5018    });
5019}
5020
5021fn init_test_with_editor(cx: &mut TestAppContext) {
5022    cx.update(|cx| {
5023        let app_state = AppState::test(cx);
5024        theme::init(theme::LoadThemes::JustBase, cx);
5025        init_settings(cx);
5026        language::init(cx);
5027        editor::init(cx);
5028        crate::init(cx);
5029        workspace::init(app_state.clone(), cx);
5030        Project::init_settings(cx);
5031
5032        cx.update_global::<SettingsStore, _>(|store, cx| {
5033            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5034                project_panel_settings.auto_fold_dirs = Some(false);
5035            });
5036            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5037                worktree_settings.file_scan_exclusions = Some(Vec::new());
5038            });
5039        });
5040    });
5041}
5042
5043fn ensure_single_file_is_opened(
5044    window: &WindowHandle<Workspace>,
5045    expected_path: &str,
5046    cx: &mut TestAppContext,
5047) {
5048    window
5049        .update(cx, |workspace, _, cx| {
5050            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5051            assert_eq!(worktrees.len(), 1);
5052            let worktree_id = worktrees[0].read(cx).id();
5053
5054            let open_project_paths = workspace
5055                .panes()
5056                .iter()
5057                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5058                .collect::<Vec<_>>();
5059            assert_eq!(
5060                open_project_paths,
5061                vec![ProjectPath {
5062                    worktree_id,
5063                    path: Arc::from(Path::new(expected_path))
5064                }],
5065                "Should have opened file, selected in project panel"
5066            );
5067        })
5068        .unwrap();
5069}
5070
5071fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5072    assert!(
5073        !cx.has_pending_prompt(),
5074        "Should have no prompts before the deletion"
5075    );
5076    panel.update_in(cx, |panel, window, cx| {
5077        panel.delete(&Delete { skip_prompt: false }, window, cx)
5078    });
5079    assert!(
5080        cx.has_pending_prompt(),
5081        "Should have a prompt after the deletion"
5082    );
5083    cx.simulate_prompt_answer("Delete");
5084    assert!(
5085        !cx.has_pending_prompt(),
5086        "Should have no prompts after prompt was replied to"
5087    );
5088    cx.executor().run_until_parked();
5089}
5090
5091fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5092    assert!(
5093        !cx.has_pending_prompt(),
5094        "Should have no prompts before the deletion"
5095    );
5096    panel.update_in(cx, |panel, window, cx| {
5097        panel.delete(&Delete { skip_prompt: true }, window, cx)
5098    });
5099    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5100    cx.executor().run_until_parked();
5101}
5102
5103fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5104    assert!(
5105        !cx.has_pending_prompt(),
5106        "Should have no prompts after deletion operation closes the file"
5107    );
5108    workspace
5109        .read_with(cx, |workspace, cx| {
5110            let open_project_paths = workspace
5111                .panes()
5112                .iter()
5113                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5114                .collect::<Vec<_>>();
5115            assert!(
5116                open_project_paths.is_empty(),
5117                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5118            );
5119        })
5120        .unwrap();
5121}
5122
5123struct TestProjectItemView {
5124    focus_handle: FocusHandle,
5125    path: ProjectPath,
5126}
5127
5128struct TestProjectItem {
5129    path: ProjectPath,
5130}
5131
5132impl project::ProjectItem for TestProjectItem {
5133    fn try_open(
5134        _project: &Entity<Project>,
5135        path: &ProjectPath,
5136        cx: &mut App,
5137    ) -> Option<Task<gpui::Result<Entity<Self>>>> {
5138        let path = path.clone();
5139        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5140    }
5141
5142    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5143        None
5144    }
5145
5146    fn project_path(&self, _: &App) -> Option<ProjectPath> {
5147        Some(self.path.clone())
5148    }
5149
5150    fn is_dirty(&self) -> bool {
5151        false
5152    }
5153}
5154
5155impl ProjectItem for TestProjectItemView {
5156    type Item = TestProjectItem;
5157
5158    fn for_project_item(
5159        _: Entity<Project>,
5160        _: &Pane,
5161        project_item: Entity<Self::Item>,
5162        _: &mut Window,
5163        cx: &mut Context<Self>,
5164    ) -> Self
5165    where
5166        Self: Sized,
5167    {
5168        Self {
5169            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5170            focus_handle: cx.focus_handle(),
5171        }
5172    }
5173}
5174
5175impl Item for TestProjectItemView {
5176    type Event = ();
5177}
5178
5179impl EventEmitter<()> for TestProjectItemView {}
5180
5181impl Focusable for TestProjectItemView {
5182    fn focus_handle(&self, _: &App) -> FocusHandle {
5183        self.focus_handle.clone()
5184    }
5185}
5186
5187impl Render for TestProjectItemView {
5188    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5189        Empty
5190    }
5191}