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    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2006    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2007    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2008
2009    // Check initial state
2010    assert_eq!(
2011        visible_entries_as_strings(&panel, 0..15, cx),
2012        &[
2013            "v tree1",
2014            "    > .git",
2015            "    > dir1",
2016            "    > dir2",
2017            "      modified4.txt",
2018            "      unmodified3.txt",
2019            "v tree2",
2020            "    > .git",
2021            "    > dir3",
2022            "      modified6.txt",
2023            "      unmodified5.txt"
2024        ],
2025    );
2026
2027    // Test selecting next modified entry
2028    panel.update_in(cx, |panel, window, cx| {
2029        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2030    });
2031
2032    assert_eq!(
2033        visible_entries_as_strings(&panel, 0..6, cx),
2034        &[
2035            "v tree1",
2036            "    > .git",
2037            "    v dir1",
2038            "          modified1.txt  <== selected",
2039            "          modified2.txt",
2040            "          unmodified1.txt",
2041        ],
2042    );
2043
2044    panel.update_in(cx, |panel, window, cx| {
2045        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2046    });
2047
2048    assert_eq!(
2049        visible_entries_as_strings(&panel, 0..6, cx),
2050        &[
2051            "v tree1",
2052            "    > .git",
2053            "    v dir1",
2054            "          modified1.txt",
2055            "          modified2.txt  <== selected",
2056            "          unmodified1.txt",
2057        ],
2058    );
2059
2060    panel.update_in(cx, |panel, window, cx| {
2061        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2062    });
2063
2064    assert_eq!(
2065        visible_entries_as_strings(&panel, 6..9, cx),
2066        &[
2067            "    v dir2",
2068            "          modified3.txt  <== selected",
2069            "          unmodified2.txt",
2070        ],
2071    );
2072
2073    panel.update_in(cx, |panel, window, cx| {
2074        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2075    });
2076
2077    assert_eq!(
2078        visible_entries_as_strings(&panel, 9..11, cx),
2079        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2080    );
2081
2082    panel.update_in(cx, |panel, window, cx| {
2083        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2084    });
2085
2086    assert_eq!(
2087        visible_entries_as_strings(&panel, 13..16, cx),
2088        &[
2089            "    v dir3",
2090            "          modified5.txt  <== selected",
2091            "          unmodified4.txt",
2092        ],
2093    );
2094
2095    panel.update_in(cx, |panel, window, cx| {
2096        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2097    });
2098
2099    assert_eq!(
2100        visible_entries_as_strings(&panel, 16..18, cx),
2101        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2102    );
2103
2104    // Wraps around to first modified file
2105    panel.update_in(cx, |panel, window, cx| {
2106        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2107    });
2108
2109    assert_eq!(
2110        visible_entries_as_strings(&panel, 0..18, cx),
2111        &[
2112            "v tree1",
2113            "    > .git",
2114            "    v dir1",
2115            "          modified1.txt  <== selected",
2116            "          modified2.txt",
2117            "          unmodified1.txt",
2118            "    v dir2",
2119            "          modified3.txt",
2120            "          unmodified2.txt",
2121            "      modified4.txt",
2122            "      unmodified3.txt",
2123            "v tree2",
2124            "    > .git",
2125            "    v dir3",
2126            "          modified5.txt",
2127            "          unmodified4.txt",
2128            "      modified6.txt",
2129            "      unmodified5.txt",
2130        ],
2131    );
2132
2133    // Wraps around again to last modified file
2134    panel.update_in(cx, |panel, window, cx| {
2135        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2136    });
2137
2138    assert_eq!(
2139        visible_entries_as_strings(&panel, 16..18, cx),
2140        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2141    );
2142
2143    panel.update_in(cx, |panel, window, cx| {
2144        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2145    });
2146
2147    assert_eq!(
2148        visible_entries_as_strings(&panel, 13..16, cx),
2149        &[
2150            "    v dir3",
2151            "          modified5.txt  <== selected",
2152            "          unmodified4.txt",
2153        ],
2154    );
2155
2156    panel.update_in(cx, |panel, window, cx| {
2157        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2158    });
2159
2160    assert_eq!(
2161        visible_entries_as_strings(&panel, 9..11, cx),
2162        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2163    );
2164
2165    panel.update_in(cx, |panel, window, cx| {
2166        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2167    });
2168
2169    assert_eq!(
2170        visible_entries_as_strings(&panel, 6..9, cx),
2171        &[
2172            "    v dir2",
2173            "          modified3.txt  <== selected",
2174            "          unmodified2.txt",
2175        ],
2176    );
2177
2178    panel.update_in(cx, |panel, window, cx| {
2179        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2180    });
2181
2182    assert_eq!(
2183        visible_entries_as_strings(&panel, 0..6, cx),
2184        &[
2185            "v tree1",
2186            "    > .git",
2187            "    v dir1",
2188            "          modified1.txt",
2189            "          modified2.txt  <== selected",
2190            "          unmodified1.txt",
2191        ],
2192    );
2193
2194    panel.update_in(cx, |panel, window, cx| {
2195        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2196    });
2197
2198    assert_eq!(
2199        visible_entries_as_strings(&panel, 0..6, cx),
2200        &[
2201            "v tree1",
2202            "    > .git",
2203            "    v dir1",
2204            "          modified1.txt  <== selected",
2205            "          modified2.txt",
2206            "          unmodified1.txt",
2207        ],
2208    );
2209}
2210
2211#[gpui::test]
2212async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2213    init_test_with_editor(cx);
2214
2215    let fs = FakeFs::new(cx.executor().clone());
2216    fs.insert_tree(
2217        "/project_root",
2218        json!({
2219            "dir_1": {
2220                "nested_dir": {
2221                    "file_a.py": "# File contents",
2222                }
2223            },
2224            "file_1.py": "# File contents",
2225            "dir_2": {
2226
2227            },
2228            "dir_3": {
2229
2230            },
2231            "file_2.py": "# File contents",
2232            "dir_4": {
2233
2234            },
2235        }),
2236    )
2237    .await;
2238
2239    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2240    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2241    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2242    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2243
2244    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2245    cx.executor().run_until_parked();
2246    select_path(&panel, "project_root/dir_1", cx);
2247    cx.executor().run_until_parked();
2248    assert_eq!(
2249        visible_entries_as_strings(&panel, 0..10, cx),
2250        &[
2251            "v project_root",
2252            "    > dir_1  <== selected",
2253            "    > dir_2",
2254            "    > dir_3",
2255            "    > dir_4",
2256            "      file_1.py",
2257            "      file_2.py",
2258        ]
2259    );
2260    panel.update_in(cx, |panel, window, cx| {
2261        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2262    });
2263
2264    assert_eq!(
2265        visible_entries_as_strings(&panel, 0..10, cx),
2266        &[
2267            "v project_root  <== selected",
2268            "    > dir_1",
2269            "    > dir_2",
2270            "    > dir_3",
2271            "    > dir_4",
2272            "      file_1.py",
2273            "      file_2.py",
2274        ]
2275    );
2276
2277    panel.update_in(cx, |panel, window, cx| {
2278        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2279    });
2280
2281    assert_eq!(
2282        visible_entries_as_strings(&panel, 0..10, cx),
2283        &[
2284            "v project_root",
2285            "    > dir_1",
2286            "    > dir_2",
2287            "    > dir_3",
2288            "    > dir_4  <== selected",
2289            "      file_1.py",
2290            "      file_2.py",
2291        ]
2292    );
2293
2294    panel.update_in(cx, |panel, window, cx| {
2295        panel.select_next_directory(&SelectNextDirectory, window, cx)
2296    });
2297
2298    assert_eq!(
2299        visible_entries_as_strings(&panel, 0..10, cx),
2300        &[
2301            "v project_root  <== selected",
2302            "    > dir_1",
2303            "    > dir_2",
2304            "    > dir_3",
2305            "    > dir_4",
2306            "      file_1.py",
2307            "      file_2.py",
2308        ]
2309    );
2310}
2311#[gpui::test]
2312async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2313    init_test_with_editor(cx);
2314
2315    let fs = FakeFs::new(cx.executor().clone());
2316    fs.insert_tree(
2317        "/project_root",
2318        json!({
2319            "dir_1": {
2320                "nested_dir": {
2321                    "file_a.py": "# File contents",
2322                }
2323            },
2324            "file_1.py": "# File contents",
2325            "file_2.py": "# File contents",
2326            "zdir_2": {
2327                "nested_dir2": {
2328                    "file_b.py": "# File contents",
2329                }
2330            },
2331        }),
2332    )
2333    .await;
2334
2335    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2336    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2337    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2338    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2339
2340    assert_eq!(
2341        visible_entries_as_strings(&panel, 0..10, cx),
2342        &[
2343            "v project_root",
2344            "    > dir_1",
2345            "    > zdir_2",
2346            "      file_1.py",
2347            "      file_2.py",
2348        ]
2349    );
2350    panel.update_in(cx, |panel, window, cx| {
2351        panel.select_first(&SelectFirst, window, cx)
2352    });
2353
2354    assert_eq!(
2355        visible_entries_as_strings(&panel, 0..10, cx),
2356        &[
2357            "v project_root  <== selected",
2358            "    > dir_1",
2359            "    > zdir_2",
2360            "      file_1.py",
2361            "      file_2.py",
2362        ]
2363    );
2364
2365    panel.update_in(cx, |panel, window, cx| {
2366        panel.select_last(&SelectLast, window, cx)
2367    });
2368
2369    assert_eq!(
2370        visible_entries_as_strings(&panel, 0..10, cx),
2371        &[
2372            "v project_root",
2373            "    > dir_1",
2374            "    > zdir_2",
2375            "      file_1.py",
2376            "      file_2.py  <== selected",
2377        ]
2378    );
2379}
2380
2381#[gpui::test]
2382async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2383    init_test_with_editor(cx);
2384
2385    let fs = FakeFs::new(cx.executor().clone());
2386    fs.insert_tree(
2387        "/project_root",
2388        json!({
2389            "dir_1": {
2390                "nested_dir": {
2391                    "file_a.py": "# File contents",
2392                }
2393            },
2394            "file_1.py": "# File contents",
2395        }),
2396    )
2397    .await;
2398
2399    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2400    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2401    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2402    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2403
2404    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2405    cx.executor().run_until_parked();
2406    select_path(&panel, "project_root/dir_1", cx);
2407    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2408    select_path(&panel, "project_root/dir_1/nested_dir", cx);
2409    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2410    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2411    cx.executor().run_until_parked();
2412    assert_eq!(
2413        visible_entries_as_strings(&panel, 0..10, cx),
2414        &[
2415            "v project_root",
2416            "    v dir_1",
2417            "        > nested_dir  <== selected",
2418            "      file_1.py",
2419        ]
2420    );
2421}
2422
2423#[gpui::test]
2424async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2425    init_test_with_editor(cx);
2426
2427    let fs = FakeFs::new(cx.executor().clone());
2428    fs.insert_tree(
2429        "/project_root",
2430        json!({
2431            "dir_1": {
2432                "nested_dir": {
2433                    "file_a.py": "# File contents",
2434                    "file_b.py": "# File contents",
2435                    "file_c.py": "# File contents",
2436                },
2437                "file_1.py": "# File contents",
2438                "file_2.py": "# File contents",
2439                "file_3.py": "# File contents",
2440            },
2441            "dir_2": {
2442                "file_1.py": "# File contents",
2443                "file_2.py": "# File contents",
2444                "file_3.py": "# File contents",
2445            }
2446        }),
2447    )
2448    .await;
2449
2450    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2451    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2452    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2453    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2454
2455    panel.update_in(cx, |panel, window, cx| {
2456        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2457    });
2458    cx.executor().run_until_parked();
2459    assert_eq!(
2460        visible_entries_as_strings(&panel, 0..10, cx),
2461        &["v project_root", "    > dir_1", "    > dir_2",]
2462    );
2463
2464    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2465    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2466    cx.executor().run_until_parked();
2467    assert_eq!(
2468        visible_entries_as_strings(&panel, 0..10, cx),
2469        &[
2470            "v project_root",
2471            "    v dir_1  <== selected",
2472            "        > nested_dir",
2473            "          file_1.py",
2474            "          file_2.py",
2475            "          file_3.py",
2476            "    > dir_2",
2477        ]
2478    );
2479}
2480
2481#[gpui::test]
2482async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2483    init_test(cx);
2484
2485    let fs = FakeFs::new(cx.executor().clone());
2486    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2487    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2488    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2489    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2490    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2491
2492    // Make a new buffer with no backing file
2493    workspace
2494        .update(cx, |workspace, window, cx| {
2495            Editor::new_file(workspace, &Default::default(), window, cx)
2496        })
2497        .unwrap();
2498
2499    cx.executor().run_until_parked();
2500
2501    // "Save as" the buffer, creating a new backing file for it
2502    let save_task = workspace
2503        .update(cx, |workspace, window, cx| {
2504            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2505        })
2506        .unwrap();
2507
2508    cx.executor().run_until_parked();
2509    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2510    save_task.await.unwrap();
2511
2512    // Rename the file
2513    select_path(&panel, "root/new", cx);
2514    assert_eq!(
2515        visible_entries_as_strings(&panel, 0..10, cx),
2516        &["v root", "      new  <== selected  <== marked"]
2517    );
2518    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2519    panel.update_in(cx, |panel, window, cx| {
2520        panel
2521            .filename_editor
2522            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2523    });
2524    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2525
2526    cx.executor().run_until_parked();
2527    assert_eq!(
2528        visible_entries_as_strings(&panel, 0..10, cx),
2529        &["v root", "      newer  <== selected"]
2530    );
2531
2532    workspace
2533        .update(cx, |workspace, window, cx| {
2534            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2535        })
2536        .unwrap()
2537        .await
2538        .unwrap();
2539
2540    cx.executor().run_until_parked();
2541    // assert that saving the file doesn't restore "new"
2542    assert_eq!(
2543        visible_entries_as_strings(&panel, 0..10, cx),
2544        &["v root", "      newer  <== selected"]
2545    );
2546}
2547
2548#[gpui::test]
2549#[cfg_attr(target_os = "windows", ignore)]
2550async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2551    init_test_with_editor(cx);
2552
2553    let fs = FakeFs::new(cx.executor().clone());
2554    fs.insert_tree(
2555        "/root1",
2556        json!({
2557            "dir1": {
2558                "file1.txt": "content 1",
2559            },
2560        }),
2561    )
2562    .await;
2563
2564    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2565    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2566    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2567    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2568
2569    toggle_expand_dir(&panel, "root1/dir1", cx);
2570
2571    assert_eq!(
2572        visible_entries_as_strings(&panel, 0..20, cx),
2573        &["v root1", "    v dir1  <== selected", "          file1.txt",],
2574        "Initial state with worktrees"
2575    );
2576
2577    select_path(&panel, "root1", cx);
2578    assert_eq!(
2579        visible_entries_as_strings(&panel, 0..20, cx),
2580        &["v root1  <== selected", "    v dir1", "          file1.txt",],
2581    );
2582
2583    // Rename root1 to new_root1
2584    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2585
2586    assert_eq!(
2587        visible_entries_as_strings(&panel, 0..20, cx),
2588        &[
2589            "v [EDITOR: 'root1']  <== selected",
2590            "    v dir1",
2591            "          file1.txt",
2592        ],
2593    );
2594
2595    let confirm = panel.update_in(cx, |panel, window, cx| {
2596        panel
2597            .filename_editor
2598            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2599        panel.confirm_edit(window, cx).unwrap()
2600    });
2601    confirm.await.unwrap();
2602    assert_eq!(
2603        visible_entries_as_strings(&panel, 0..20, cx),
2604        &[
2605            "v new_root1  <== selected",
2606            "    v dir1",
2607            "          file1.txt",
2608        ],
2609        "Should update worktree name"
2610    );
2611
2612    // Ensure internal paths have been updated
2613    select_path(&panel, "new_root1/dir1/file1.txt", cx);
2614    assert_eq!(
2615        visible_entries_as_strings(&panel, 0..20, cx),
2616        &[
2617            "v new_root1",
2618            "    v dir1",
2619            "          file1.txt  <== selected",
2620        ],
2621        "Files in renamed worktree are selectable"
2622    );
2623}
2624
2625#[gpui::test]
2626async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2627    init_test_with_editor(cx);
2628    let fs = FakeFs::new(cx.executor().clone());
2629    fs.insert_tree(
2630        "/project_root",
2631        json!({
2632            "dir_1": {
2633                "nested_dir": {
2634                    "file_a.py": "# File contents",
2635                }
2636            },
2637            "file_1.py": "# File contents",
2638        }),
2639    )
2640    .await;
2641
2642    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2643    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2644    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2645    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2646    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2647    cx.update(|window, cx| {
2648        panel.update(cx, |this, cx| {
2649            this.select_next(&Default::default(), window, cx);
2650            this.expand_selected_entry(&Default::default(), window, cx);
2651            this.expand_selected_entry(&Default::default(), window, cx);
2652            this.select_next(&Default::default(), window, cx);
2653            this.expand_selected_entry(&Default::default(), window, cx);
2654            this.select_next(&Default::default(), window, cx);
2655        })
2656    });
2657    assert_eq!(
2658        visible_entries_as_strings(&panel, 0..10, cx),
2659        &[
2660            "v project_root",
2661            "    v dir_1",
2662            "        v nested_dir",
2663            "              file_a.py  <== selected",
2664            "      file_1.py",
2665        ]
2666    );
2667    let modifiers_with_shift = gpui::Modifiers {
2668        shift: true,
2669        ..Default::default()
2670    };
2671    cx.simulate_modifiers_change(modifiers_with_shift);
2672    cx.update(|window, cx| {
2673        panel.update(cx, |this, cx| {
2674            this.select_next(&Default::default(), window, cx);
2675        })
2676    });
2677    assert_eq!(
2678        visible_entries_as_strings(&panel, 0..10, cx),
2679        &[
2680            "v project_root",
2681            "    v dir_1",
2682            "        v nested_dir",
2683            "              file_a.py",
2684            "      file_1.py  <== selected  <== marked",
2685        ]
2686    );
2687    cx.update(|window, cx| {
2688        panel.update(cx, |this, cx| {
2689            this.select_previous(&Default::default(), window, cx);
2690        })
2691    });
2692    assert_eq!(
2693        visible_entries_as_strings(&panel, 0..10, cx),
2694        &[
2695            "v project_root",
2696            "    v dir_1",
2697            "        v nested_dir",
2698            "              file_a.py  <== selected  <== marked",
2699            "      file_1.py  <== marked",
2700        ]
2701    );
2702    cx.update(|window, cx| {
2703        panel.update(cx, |this, cx| {
2704            let drag = DraggedSelection {
2705                active_selection: this.selection.unwrap(),
2706                marked_selections: Arc::new(this.marked_entries.clone()),
2707            };
2708            let target_entry = this
2709                .project
2710                .read(cx)
2711                .entry_for_path(&(worktree_id, "").into(), cx)
2712                .unwrap();
2713            this.drag_onto(&drag, target_entry.id, false, window, cx);
2714        });
2715    });
2716    cx.run_until_parked();
2717    assert_eq!(
2718        visible_entries_as_strings(&panel, 0..10, cx),
2719        &[
2720            "v project_root",
2721            "    v dir_1",
2722            "        v nested_dir",
2723            "      file_1.py  <== marked",
2724            "      file_a.py  <== selected  <== marked",
2725        ]
2726    );
2727    // ESC clears out all marks
2728    cx.update(|window, cx| {
2729        panel.update(cx, |this, cx| {
2730            this.cancel(&menu::Cancel, window, cx);
2731        })
2732    });
2733    assert_eq!(
2734        visible_entries_as_strings(&panel, 0..10, cx),
2735        &[
2736            "v project_root",
2737            "    v dir_1",
2738            "        v nested_dir",
2739            "      file_1.py",
2740            "      file_a.py  <== selected",
2741        ]
2742    );
2743    // ESC clears out all marks
2744    cx.update(|window, cx| {
2745        panel.update(cx, |this, cx| {
2746            this.select_previous(&SelectPrevious, window, cx);
2747            this.select_next(&SelectNext, window, cx);
2748        })
2749    });
2750    assert_eq!(
2751        visible_entries_as_strings(&panel, 0..10, cx),
2752        &[
2753            "v project_root",
2754            "    v dir_1",
2755            "        v nested_dir",
2756            "      file_1.py  <== marked",
2757            "      file_a.py  <== selected  <== marked",
2758        ]
2759    );
2760    cx.simulate_modifiers_change(Default::default());
2761    cx.update(|window, cx| {
2762        panel.update(cx, |this, cx| {
2763            this.cut(&Cut, window, cx);
2764            this.select_previous(&SelectPrevious, window, cx);
2765            this.select_previous(&SelectPrevious, window, cx);
2766
2767            this.paste(&Paste, window, cx);
2768            // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2769        })
2770    });
2771    cx.run_until_parked();
2772    assert_eq!(
2773        visible_entries_as_strings(&panel, 0..10, cx),
2774        &[
2775            "v project_root",
2776            "    v dir_1",
2777            "        v nested_dir",
2778            "              file_1.py  <== marked",
2779            "              file_a.py  <== selected  <== marked",
2780        ]
2781    );
2782    cx.simulate_modifiers_change(modifiers_with_shift);
2783    cx.update(|window, cx| {
2784        panel.update(cx, |this, cx| {
2785            this.expand_selected_entry(&Default::default(), window, cx);
2786            this.select_next(&SelectNext, window, cx);
2787            this.select_next(&SelectNext, window, cx);
2788        })
2789    });
2790    submit_deletion(&panel, cx);
2791    assert_eq!(
2792        visible_entries_as_strings(&panel, 0..10, cx),
2793        &[
2794            "v project_root",
2795            "    v dir_1",
2796            "        v nested_dir  <== selected",
2797        ]
2798    );
2799}
2800#[gpui::test]
2801async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2802    init_test_with_editor(cx);
2803    cx.update(|cx| {
2804        cx.update_global::<SettingsStore, _>(|store, cx| {
2805            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2806                worktree_settings.file_scan_exclusions = Some(Vec::new());
2807            });
2808            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2809                project_panel_settings.auto_reveal_entries = Some(false)
2810            });
2811        })
2812    });
2813
2814    let fs = FakeFs::new(cx.background_executor.clone());
2815    fs.insert_tree(
2816        "/project_root",
2817        json!({
2818            ".git": {},
2819            ".gitignore": "**/gitignored_dir",
2820            "dir_1": {
2821                "file_1.py": "# File 1_1 contents",
2822                "file_2.py": "# File 1_2 contents",
2823                "file_3.py": "# File 1_3 contents",
2824                "gitignored_dir": {
2825                    "file_a.py": "# File contents",
2826                    "file_b.py": "# File contents",
2827                    "file_c.py": "# File contents",
2828                },
2829            },
2830            "dir_2": {
2831                "file_1.py": "# File 2_1 contents",
2832                "file_2.py": "# File 2_2 contents",
2833                "file_3.py": "# File 2_3 contents",
2834            }
2835        }),
2836    )
2837    .await;
2838
2839    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2840    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2841    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2842    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2843
2844    assert_eq!(
2845        visible_entries_as_strings(&panel, 0..20, cx),
2846        &[
2847            "v project_root",
2848            "    > .git",
2849            "    > dir_1",
2850            "    > dir_2",
2851            "      .gitignore",
2852        ]
2853    );
2854
2855    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
2856        .expect("dir 1 file is not ignored and should have an entry");
2857    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
2858        .expect("dir 2 file is not ignored and should have an entry");
2859    let gitignored_dir_file =
2860        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
2861    assert_eq!(
2862        gitignored_dir_file, None,
2863        "File in the gitignored dir should not have an entry before its dir is toggled"
2864    );
2865
2866    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2867    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2868    cx.executor().run_until_parked();
2869    assert_eq!(
2870        visible_entries_as_strings(&panel, 0..20, cx),
2871        &[
2872            "v project_root",
2873            "    > .git",
2874            "    v dir_1",
2875            "        v gitignored_dir  <== selected",
2876            "              file_a.py",
2877            "              file_b.py",
2878            "              file_c.py",
2879            "          file_1.py",
2880            "          file_2.py",
2881            "          file_3.py",
2882            "    > dir_2",
2883            "      .gitignore",
2884        ],
2885        "Should show gitignored dir file list in the project panel"
2886    );
2887    let gitignored_dir_file =
2888        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
2889            .expect("after gitignored dir got opened, a file entry should be present");
2890
2891    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2892    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2893    assert_eq!(
2894        visible_entries_as_strings(&panel, 0..20, cx),
2895        &[
2896            "v project_root",
2897            "    > .git",
2898            "    > dir_1  <== selected",
2899            "    > dir_2",
2900            "      .gitignore",
2901        ],
2902        "Should hide all dir contents again and prepare for the auto reveal test"
2903    );
2904
2905    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
2906        panel.update(cx, |panel, cx| {
2907            panel.project.update(cx, |_, cx| {
2908                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
2909            })
2910        });
2911        cx.run_until_parked();
2912        assert_eq!(
2913            visible_entries_as_strings(&panel, 0..20, cx),
2914            &[
2915                "v project_root",
2916                "    > .git",
2917                "    > dir_1  <== selected",
2918                "    > dir_2",
2919                "      .gitignore",
2920            ],
2921            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
2922        );
2923    }
2924
2925    cx.update(|_, cx| {
2926        cx.update_global::<SettingsStore, _>(|store, cx| {
2927            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2928                project_panel_settings.auto_reveal_entries = Some(true)
2929            });
2930        })
2931    });
2932
2933    panel.update(cx, |panel, cx| {
2934        panel.project.update(cx, |_, cx| {
2935            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
2936        })
2937    });
2938    cx.run_until_parked();
2939    assert_eq!(
2940        visible_entries_as_strings(&panel, 0..20, cx),
2941        &[
2942            "v project_root",
2943            "    > .git",
2944            "    v dir_1",
2945            "        > gitignored_dir",
2946            "          file_1.py  <== selected  <== marked",
2947            "          file_2.py",
2948            "          file_3.py",
2949            "    > dir_2",
2950            "      .gitignore",
2951        ],
2952        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
2953    );
2954
2955    panel.update(cx, |panel, cx| {
2956        panel.project.update(cx, |_, cx| {
2957            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
2958        })
2959    });
2960    cx.run_until_parked();
2961    assert_eq!(
2962        visible_entries_as_strings(&panel, 0..20, cx),
2963        &[
2964            "v project_root",
2965            "    > .git",
2966            "    v dir_1",
2967            "        > gitignored_dir",
2968            "          file_1.py",
2969            "          file_2.py",
2970            "          file_3.py",
2971            "    v dir_2",
2972            "          file_1.py  <== selected  <== marked",
2973            "          file_2.py",
2974            "          file_3.py",
2975            "      .gitignore",
2976        ],
2977        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
2978    );
2979
2980    panel.update(cx, |panel, cx| {
2981        panel.project.update(cx, |_, cx| {
2982            cx.emit(project::Event::ActiveEntryChanged(Some(
2983                gitignored_dir_file,
2984            )))
2985        })
2986    });
2987    cx.run_until_parked();
2988    assert_eq!(
2989        visible_entries_as_strings(&panel, 0..20, cx),
2990        &[
2991            "v project_root",
2992            "    > .git",
2993            "    v dir_1",
2994            "        > gitignored_dir",
2995            "          file_1.py",
2996            "          file_2.py",
2997            "          file_3.py",
2998            "    v dir_2",
2999            "          file_1.py  <== selected  <== marked",
3000            "          file_2.py",
3001            "          file_3.py",
3002            "      .gitignore",
3003        ],
3004        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3005    );
3006
3007    panel.update(cx, |panel, cx| {
3008        panel.project.update(cx, |_, cx| {
3009            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3010        })
3011    });
3012    cx.run_until_parked();
3013    assert_eq!(
3014        visible_entries_as_strings(&panel, 0..20, cx),
3015        &[
3016            "v project_root",
3017            "    > .git",
3018            "    v dir_1",
3019            "        v gitignored_dir",
3020            "              file_a.py  <== selected  <== marked",
3021            "              file_b.py",
3022            "              file_c.py",
3023            "          file_1.py",
3024            "          file_2.py",
3025            "          file_3.py",
3026            "    v dir_2",
3027            "          file_1.py",
3028            "          file_2.py",
3029            "          file_3.py",
3030            "      .gitignore",
3031        ],
3032        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3033    );
3034}
3035
3036#[gpui::test]
3037async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3038    init_test_with_editor(cx);
3039    cx.update(|cx| {
3040        cx.update_global::<SettingsStore, _>(|store, cx| {
3041            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3042                worktree_settings.file_scan_exclusions = Some(Vec::new());
3043                worktree_settings.file_scan_inclusions =
3044                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3045            });
3046            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3047                project_panel_settings.auto_reveal_entries = Some(false)
3048            });
3049        })
3050    });
3051
3052    let fs = FakeFs::new(cx.background_executor.clone());
3053    fs.insert_tree(
3054        "/project_root",
3055        json!({
3056            ".git": {},
3057            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3058            "dir_1": {
3059                "file_1.py": "# File 1_1 contents",
3060                "file_2.py": "# File 1_2 contents",
3061                "file_3.py": "# File 1_3 contents",
3062                "gitignored_dir": {
3063                    "file_a.py": "# File contents",
3064                    "file_b.py": "# File contents",
3065                    "file_c.py": "# File contents",
3066                },
3067            },
3068            "dir_2": {
3069                "file_1.py": "# File 2_1 contents",
3070                "file_2.py": "# File 2_2 contents",
3071                "file_3.py": "# File 2_3 contents",
3072            },
3073            "always_included_but_ignored_dir": {
3074                "file_a.py": "# File contents",
3075                "file_b.py": "# File contents",
3076                "file_c.py": "# File contents",
3077            },
3078        }),
3079    )
3080    .await;
3081
3082    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3083    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3084    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3085    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3086
3087    assert_eq!(
3088        visible_entries_as_strings(&panel, 0..20, cx),
3089        &[
3090            "v project_root",
3091            "    > .git",
3092            "    > always_included_but_ignored_dir",
3093            "    > dir_1",
3094            "    > dir_2",
3095            "      .gitignore",
3096        ]
3097    );
3098
3099    let gitignored_dir_file =
3100        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3101    let always_included_but_ignored_dir_file = find_project_entry(
3102        &panel,
3103        "project_root/always_included_but_ignored_dir/file_a.py",
3104        cx,
3105    )
3106    .expect("file that is .gitignored but set to always be included should have an entry");
3107    assert_eq!(
3108        gitignored_dir_file, None,
3109        "File in the gitignored dir should not have an entry unless its directory is toggled"
3110    );
3111
3112    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3113    cx.run_until_parked();
3114    cx.update(|_, cx| {
3115        cx.update_global::<SettingsStore, _>(|store, cx| {
3116            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3117                project_panel_settings.auto_reveal_entries = Some(true)
3118            });
3119        })
3120    });
3121
3122    panel.update(cx, |panel, cx| {
3123        panel.project.update(cx, |_, cx| {
3124            cx.emit(project::Event::ActiveEntryChanged(Some(
3125                always_included_but_ignored_dir_file,
3126            )))
3127        })
3128    });
3129    cx.run_until_parked();
3130
3131    assert_eq!(
3132        visible_entries_as_strings(&panel, 0..20, cx),
3133        &[
3134            "v project_root",
3135            "    > .git",
3136            "    v always_included_but_ignored_dir",
3137            "          file_a.py  <== selected  <== marked",
3138            "          file_b.py",
3139            "          file_c.py",
3140            "    v dir_1",
3141            "        > gitignored_dir",
3142            "          file_1.py",
3143            "          file_2.py",
3144            "          file_3.py",
3145            "    > dir_2",
3146            "      .gitignore",
3147        ],
3148        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3149    );
3150}
3151
3152#[gpui::test]
3153async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3154    init_test_with_editor(cx);
3155    cx.update(|cx| {
3156        cx.update_global::<SettingsStore, _>(|store, cx| {
3157            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3158                worktree_settings.file_scan_exclusions = Some(Vec::new());
3159            });
3160            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3161                project_panel_settings.auto_reveal_entries = Some(false)
3162            });
3163        })
3164    });
3165
3166    let fs = FakeFs::new(cx.background_executor.clone());
3167    fs.insert_tree(
3168        "/project_root",
3169        json!({
3170            ".git": {},
3171            ".gitignore": "**/gitignored_dir",
3172            "dir_1": {
3173                "file_1.py": "# File 1_1 contents",
3174                "file_2.py": "# File 1_2 contents",
3175                "file_3.py": "# File 1_3 contents",
3176                "gitignored_dir": {
3177                    "file_a.py": "# File contents",
3178                    "file_b.py": "# File contents",
3179                    "file_c.py": "# File contents",
3180                },
3181            },
3182            "dir_2": {
3183                "file_1.py": "# File 2_1 contents",
3184                "file_2.py": "# File 2_2 contents",
3185                "file_3.py": "# File 2_3 contents",
3186            }
3187        }),
3188    )
3189    .await;
3190
3191    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3192    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3193    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3194    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3195
3196    assert_eq!(
3197        visible_entries_as_strings(&panel, 0..20, cx),
3198        &[
3199            "v project_root",
3200            "    > .git",
3201            "    > dir_1",
3202            "    > dir_2",
3203            "      .gitignore",
3204        ]
3205    );
3206
3207    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3208        .expect("dir 1 file is not ignored and should have an entry");
3209    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3210        .expect("dir 2 file is not ignored and should have an entry");
3211    let gitignored_dir_file =
3212        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3213    assert_eq!(
3214        gitignored_dir_file, None,
3215        "File in the gitignored dir should not have an entry before its dir is toggled"
3216    );
3217
3218    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3219    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3220    cx.run_until_parked();
3221    assert_eq!(
3222        visible_entries_as_strings(&panel, 0..20, cx),
3223        &[
3224            "v project_root",
3225            "    > .git",
3226            "    v dir_1",
3227            "        v gitignored_dir  <== selected",
3228            "              file_a.py",
3229            "              file_b.py",
3230            "              file_c.py",
3231            "          file_1.py",
3232            "          file_2.py",
3233            "          file_3.py",
3234            "    > dir_2",
3235            "      .gitignore",
3236        ],
3237        "Should show gitignored dir file list in the project panel"
3238    );
3239    let gitignored_dir_file =
3240        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3241            .expect("after gitignored dir got opened, a file entry should be present");
3242
3243    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3244    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3245    assert_eq!(
3246        visible_entries_as_strings(&panel, 0..20, cx),
3247        &[
3248            "v project_root",
3249            "    > .git",
3250            "    > dir_1  <== selected",
3251            "    > dir_2",
3252            "      .gitignore",
3253        ],
3254        "Should hide all dir contents again and prepare for the explicit reveal test"
3255    );
3256
3257    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3258        panel.update(cx, |panel, cx| {
3259            panel.project.update(cx, |_, cx| {
3260                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3261            })
3262        });
3263        cx.run_until_parked();
3264        assert_eq!(
3265            visible_entries_as_strings(&panel, 0..20, cx),
3266            &[
3267                "v project_root",
3268                "    > .git",
3269                "    > dir_1  <== selected",
3270                "    > dir_2",
3271                "      .gitignore",
3272            ],
3273            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3274        );
3275    }
3276
3277    panel.update(cx, |panel, cx| {
3278        panel.project.update(cx, |_, cx| {
3279            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3280        })
3281    });
3282    cx.run_until_parked();
3283    assert_eq!(
3284        visible_entries_as_strings(&panel, 0..20, cx),
3285        &[
3286            "v project_root",
3287            "    > .git",
3288            "    v dir_1",
3289            "        > gitignored_dir",
3290            "          file_1.py  <== selected  <== marked",
3291            "          file_2.py",
3292            "          file_3.py",
3293            "    > dir_2",
3294            "      .gitignore",
3295        ],
3296        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3297    );
3298
3299    panel.update(cx, |panel, cx| {
3300        panel.project.update(cx, |_, cx| {
3301            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3302        })
3303    });
3304    cx.run_until_parked();
3305    assert_eq!(
3306        visible_entries_as_strings(&panel, 0..20, cx),
3307        &[
3308            "v project_root",
3309            "    > .git",
3310            "    v dir_1",
3311            "        > gitignored_dir",
3312            "          file_1.py",
3313            "          file_2.py",
3314            "          file_3.py",
3315            "    v dir_2",
3316            "          file_1.py  <== selected  <== marked",
3317            "          file_2.py",
3318            "          file_3.py",
3319            "      .gitignore",
3320        ],
3321        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3322    );
3323
3324    panel.update(cx, |panel, cx| {
3325        panel.project.update(cx, |_, cx| {
3326            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3327        })
3328    });
3329    cx.run_until_parked();
3330    assert_eq!(
3331        visible_entries_as_strings(&panel, 0..20, cx),
3332        &[
3333            "v project_root",
3334            "    > .git",
3335            "    v dir_1",
3336            "        v gitignored_dir",
3337            "              file_a.py  <== selected  <== marked",
3338            "              file_b.py",
3339            "              file_c.py",
3340            "          file_1.py",
3341            "          file_2.py",
3342            "          file_3.py",
3343            "    v dir_2",
3344            "          file_1.py",
3345            "          file_2.py",
3346            "          file_3.py",
3347            "      .gitignore",
3348        ],
3349        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3350    );
3351}
3352
3353#[gpui::test]
3354async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3355    init_test(cx);
3356    cx.update(|cx| {
3357        cx.update_global::<SettingsStore, _>(|store, cx| {
3358            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3359                project_settings.file_scan_exclusions =
3360                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3361            });
3362        });
3363    });
3364
3365    cx.update(|cx| {
3366        register_project_item::<TestProjectItemView>(cx);
3367    });
3368
3369    let fs = FakeFs::new(cx.executor().clone());
3370    fs.insert_tree(
3371        "/root1",
3372        json!({
3373            ".dockerignore": "",
3374            ".git": {
3375                "HEAD": "",
3376            },
3377        }),
3378    )
3379    .await;
3380
3381    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3382    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3383    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3384    let panel = workspace
3385        .update(cx, |workspace, window, cx| {
3386            let panel = ProjectPanel::new(workspace, window, cx);
3387            workspace.add_panel(panel.clone(), window, cx);
3388            panel
3389        })
3390        .unwrap();
3391
3392    select_path(&panel, "root1", cx);
3393    assert_eq!(
3394        visible_entries_as_strings(&panel, 0..10, cx),
3395        &["v root1  <== selected", "      .dockerignore",]
3396    );
3397    workspace
3398        .update(cx, |workspace, _, cx| {
3399            assert!(
3400                workspace.active_item(cx).is_none(),
3401                "Should have no active items in the beginning"
3402            );
3403        })
3404        .unwrap();
3405
3406    let excluded_file_path = ".git/COMMIT_EDITMSG";
3407    let excluded_dir_path = "excluded_dir";
3408
3409    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3410    panel.update_in(cx, |panel, window, cx| {
3411        assert!(panel.filename_editor.read(cx).is_focused(window));
3412    });
3413    panel
3414        .update_in(cx, |panel, window, cx| {
3415            panel.filename_editor.update(cx, |editor, cx| {
3416                editor.set_text(excluded_file_path, window, cx)
3417            });
3418            panel.confirm_edit(window, cx).unwrap()
3419        })
3420        .await
3421        .unwrap();
3422
3423    assert_eq!(
3424        visible_entries_as_strings(&panel, 0..13, cx),
3425        &["v root1", "      .dockerignore"],
3426        "Excluded dir should not be shown after opening a file in it"
3427    );
3428    panel.update_in(cx, |panel, window, cx| {
3429        assert!(
3430            !panel.filename_editor.read(cx).is_focused(window),
3431            "Should have closed the file name editor"
3432        );
3433    });
3434    workspace
3435        .update(cx, |workspace, _, cx| {
3436            let active_entry_path = workspace
3437                .active_item(cx)
3438                .expect("should have opened and activated the excluded item")
3439                .act_as::<TestProjectItemView>(cx)
3440                .expect("should have opened the corresponding project item for the excluded item")
3441                .read(cx)
3442                .path
3443                .clone();
3444            assert_eq!(
3445                active_entry_path.path.as_ref(),
3446                Path::new(excluded_file_path),
3447                "Should open the excluded file"
3448            );
3449
3450            assert!(
3451                workspace.notification_ids().is_empty(),
3452                "Should have no notifications after opening an excluded file"
3453            );
3454        })
3455        .unwrap();
3456    assert!(
3457        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3458        "Should have created the excluded file"
3459    );
3460
3461    select_path(&panel, "root1", cx);
3462    panel.update_in(cx, |panel, window, cx| {
3463        panel.new_directory(&NewDirectory, window, cx)
3464    });
3465    panel.update_in(cx, |panel, window, cx| {
3466        assert!(panel.filename_editor.read(cx).is_focused(window));
3467    });
3468    panel
3469        .update_in(cx, |panel, window, cx| {
3470            panel.filename_editor.update(cx, |editor, cx| {
3471                editor.set_text(excluded_file_path, window, cx)
3472            });
3473            panel.confirm_edit(window, cx).unwrap()
3474        })
3475        .await
3476        .unwrap();
3477
3478    assert_eq!(
3479        visible_entries_as_strings(&panel, 0..13, cx),
3480        &["v root1", "      .dockerignore"],
3481        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3482    );
3483    panel.update_in(cx, |panel, window, cx| {
3484        assert!(
3485            !panel.filename_editor.read(cx).is_focused(window),
3486            "Should have closed the file name editor"
3487        );
3488    });
3489    workspace
3490        .update(cx, |workspace, _, cx| {
3491            let notifications = workspace.notification_ids();
3492            assert_eq!(
3493                notifications.len(),
3494                1,
3495                "Should receive one notification with the error message"
3496            );
3497            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3498            assert!(workspace.notification_ids().is_empty());
3499        })
3500        .unwrap();
3501
3502    select_path(&panel, "root1", cx);
3503    panel.update_in(cx, |panel, window, cx| {
3504        panel.new_directory(&NewDirectory, window, cx)
3505    });
3506    panel.update_in(cx, |panel, window, cx| {
3507        assert!(panel.filename_editor.read(cx).is_focused(window));
3508    });
3509    panel
3510        .update_in(cx, |panel, window, cx| {
3511            panel.filename_editor.update(cx, |editor, cx| {
3512                editor.set_text(excluded_dir_path, window, cx)
3513            });
3514            panel.confirm_edit(window, cx).unwrap()
3515        })
3516        .await
3517        .unwrap();
3518
3519    assert_eq!(
3520        visible_entries_as_strings(&panel, 0..13, cx),
3521        &["v root1", "      .dockerignore"],
3522        "Should not change the project panel after trying to create an excluded directory"
3523    );
3524    panel.update_in(cx, |panel, window, cx| {
3525        assert!(
3526            !panel.filename_editor.read(cx).is_focused(window),
3527            "Should have closed the file name editor"
3528        );
3529    });
3530    workspace
3531        .update(cx, |workspace, _, cx| {
3532            let notifications = workspace.notification_ids();
3533            assert_eq!(
3534                notifications.len(),
3535                1,
3536                "Should receive one notification explaining that no directory is actually shown"
3537            );
3538            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3539            assert!(workspace.notification_ids().is_empty());
3540        })
3541        .unwrap();
3542    assert!(
3543        fs.is_dir(Path::new("/root1/excluded_dir")).await,
3544        "Should have created the excluded directory"
3545    );
3546}
3547
3548#[gpui::test]
3549async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3550    init_test_with_editor(cx);
3551
3552    let fs = FakeFs::new(cx.executor().clone());
3553    fs.insert_tree(
3554        "/src",
3555        json!({
3556            "test": {
3557                "first.rs": "// First Rust file",
3558                "second.rs": "// Second Rust file",
3559                "third.rs": "// Third Rust file",
3560            }
3561        }),
3562    )
3563    .await;
3564
3565    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3566    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3567    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3568    let panel = workspace
3569        .update(cx, |workspace, window, cx| {
3570            let panel = ProjectPanel::new(workspace, window, cx);
3571            workspace.add_panel(panel.clone(), window, cx);
3572            panel
3573        })
3574        .unwrap();
3575
3576    select_path(&panel, "src/", cx);
3577    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3578    cx.executor().run_until_parked();
3579    assert_eq!(
3580        visible_entries_as_strings(&panel, 0..10, cx),
3581        &[
3582            //
3583            "v src  <== selected",
3584            "    > test"
3585        ]
3586    );
3587    panel.update_in(cx, |panel, window, cx| {
3588        panel.new_directory(&NewDirectory, window, cx)
3589    });
3590    panel.update_in(cx, |panel, window, cx| {
3591        assert!(panel.filename_editor.read(cx).is_focused(window));
3592    });
3593    assert_eq!(
3594        visible_entries_as_strings(&panel, 0..10, cx),
3595        &[
3596            //
3597            "v src",
3598            "    > [EDITOR: '']  <== selected",
3599            "    > test"
3600        ]
3601    );
3602
3603    panel.update_in(cx, |panel, window, cx| {
3604        panel.cancel(&menu::Cancel, window, cx)
3605    });
3606    assert_eq!(
3607        visible_entries_as_strings(&panel, 0..10, cx),
3608        &[
3609            //
3610            "v src  <== selected",
3611            "    > test"
3612        ]
3613    );
3614}
3615
3616#[gpui::test]
3617async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3618    init_test_with_editor(cx);
3619
3620    let fs = FakeFs::new(cx.executor().clone());
3621    fs.insert_tree(
3622        "/root",
3623        json!({
3624            "dir1": {
3625                "subdir1": {},
3626                "file1.txt": "",
3627                "file2.txt": "",
3628            },
3629            "dir2": {
3630                "subdir2": {},
3631                "file3.txt": "",
3632                "file4.txt": "",
3633            },
3634            "file5.txt": "",
3635            "file6.txt": "",
3636        }),
3637    )
3638    .await;
3639
3640    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3641    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3642    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3643    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3644
3645    toggle_expand_dir(&panel, "root/dir1", cx);
3646    toggle_expand_dir(&panel, "root/dir2", cx);
3647
3648    // Test Case 1: Delete middle file in directory
3649    select_path(&panel, "root/dir1/file1.txt", cx);
3650    assert_eq!(
3651        visible_entries_as_strings(&panel, 0..15, cx),
3652        &[
3653            "v root",
3654            "    v dir1",
3655            "        > subdir1",
3656            "          file1.txt  <== selected",
3657            "          file2.txt",
3658            "    v dir2",
3659            "        > subdir2",
3660            "          file3.txt",
3661            "          file4.txt",
3662            "      file5.txt",
3663            "      file6.txt",
3664        ],
3665        "Initial state before deleting middle file"
3666    );
3667
3668    submit_deletion(&panel, cx);
3669    assert_eq!(
3670        visible_entries_as_strings(&panel, 0..15, cx),
3671        &[
3672            "v root",
3673            "    v dir1",
3674            "        > subdir1",
3675            "          file2.txt  <== selected",
3676            "    v dir2",
3677            "        > subdir2",
3678            "          file3.txt",
3679            "          file4.txt",
3680            "      file5.txt",
3681            "      file6.txt",
3682        ],
3683        "Should select next file after deleting middle file"
3684    );
3685
3686    // Test Case 2: Delete last file in directory
3687    submit_deletion(&panel, cx);
3688    assert_eq!(
3689        visible_entries_as_strings(&panel, 0..15, cx),
3690        &[
3691            "v root",
3692            "    v dir1",
3693            "        > subdir1  <== selected",
3694            "    v dir2",
3695            "        > subdir2",
3696            "          file3.txt",
3697            "          file4.txt",
3698            "      file5.txt",
3699            "      file6.txt",
3700        ],
3701        "Should select next directory when last file is deleted"
3702    );
3703
3704    // Test Case 3: Delete root level file
3705    select_path(&panel, "root/file6.txt", cx);
3706    assert_eq!(
3707        visible_entries_as_strings(&panel, 0..15, cx),
3708        &[
3709            "v root",
3710            "    v dir1",
3711            "        > subdir1",
3712            "    v dir2",
3713            "        > subdir2",
3714            "          file3.txt",
3715            "          file4.txt",
3716            "      file5.txt",
3717            "      file6.txt  <== selected",
3718        ],
3719        "Initial state before deleting root level file"
3720    );
3721
3722    submit_deletion(&panel, cx);
3723    assert_eq!(
3724        visible_entries_as_strings(&panel, 0..15, cx),
3725        &[
3726            "v root",
3727            "    v dir1",
3728            "        > subdir1",
3729            "    v dir2",
3730            "        > subdir2",
3731            "          file3.txt",
3732            "          file4.txt",
3733            "      file5.txt  <== selected",
3734        ],
3735        "Should select prev entry at root level"
3736    );
3737}
3738
3739#[gpui::test]
3740async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3741    init_test_with_editor(cx);
3742
3743    let fs = FakeFs::new(cx.executor().clone());
3744    fs.insert_tree(
3745        path!("/root"),
3746        json!({
3747            "aa": "// Testing 1",
3748            "bb": "// Testing 2",
3749            "cc": "// Testing 3",
3750            "dd": "// Testing 4",
3751            "ee": "// Testing 5",
3752            "ff": "// Testing 6",
3753            "gg": "// Testing 7",
3754            "hh": "// Testing 8",
3755            "ii": "// Testing 8",
3756            ".gitignore": "bb\ndd\nee\nff\nii\n'",
3757        }),
3758    )
3759    .await;
3760
3761    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3762    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3763    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3764
3765    // Test 1: Auto selection with one gitignored file next to the deleted file
3766    cx.update(|_, cx| {
3767        let settings = *ProjectPanelSettings::get_global(cx);
3768        ProjectPanelSettings::override_global(
3769            ProjectPanelSettings {
3770                hide_gitignore: true,
3771                ..settings
3772            },
3773            cx,
3774        );
3775    });
3776
3777    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3778
3779    select_path(&panel, "root/aa", cx);
3780    assert_eq!(
3781        visible_entries_as_strings(&panel, 0..10, cx),
3782        &[
3783            "v root",
3784            "      .gitignore",
3785            "      aa  <== selected",
3786            "      cc",
3787            "      gg",
3788            "      hh"
3789        ],
3790        "Initial state should hide files on .gitignore"
3791    );
3792
3793    submit_deletion(&panel, cx);
3794
3795    assert_eq!(
3796        visible_entries_as_strings(&panel, 0..10, cx),
3797        &[
3798            "v root",
3799            "      .gitignore",
3800            "      cc  <== selected",
3801            "      gg",
3802            "      hh"
3803        ],
3804        "Should select next entry not on .gitignore"
3805    );
3806
3807    // Test 2: Auto selection with many gitignored files next to the deleted file
3808    submit_deletion(&panel, cx);
3809    assert_eq!(
3810        visible_entries_as_strings(&panel, 0..10, cx),
3811        &[
3812            "v root",
3813            "      .gitignore",
3814            "      gg  <== selected",
3815            "      hh"
3816        ],
3817        "Should select next entry not on .gitignore"
3818    );
3819
3820    // Test 3: Auto selection of entry before deleted file
3821    select_path(&panel, "root/hh", cx);
3822    assert_eq!(
3823        visible_entries_as_strings(&panel, 0..10, cx),
3824        &[
3825            "v root",
3826            "      .gitignore",
3827            "      gg",
3828            "      hh  <== selected"
3829        ],
3830        "Should select next entry not on .gitignore"
3831    );
3832    submit_deletion(&panel, cx);
3833    assert_eq!(
3834        visible_entries_as_strings(&panel, 0..10, cx),
3835        &["v root", "      .gitignore", "      gg  <== selected"],
3836        "Should select next entry not on .gitignore"
3837    );
3838}
3839
3840#[gpui::test]
3841async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
3842    init_test_with_editor(cx);
3843
3844    let fs = FakeFs::new(cx.executor().clone());
3845    fs.insert_tree(
3846        path!("/root"),
3847        json!({
3848            "dir1": {
3849                "file1": "// Testing",
3850                "file2": "// Testing",
3851                "file3": "// Testing"
3852            },
3853            "aa": "// Testing",
3854            ".gitignore": "file1\nfile3\n",
3855        }),
3856    )
3857    .await;
3858
3859    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3860    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3861    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3862
3863    cx.update(|_, cx| {
3864        let settings = *ProjectPanelSettings::get_global(cx);
3865        ProjectPanelSettings::override_global(
3866            ProjectPanelSettings {
3867                hide_gitignore: true,
3868                ..settings
3869            },
3870            cx,
3871        );
3872    });
3873
3874    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3875
3876    // Test 1: Visible items should exclude files on gitignore
3877    toggle_expand_dir(&panel, "root/dir1", cx);
3878    select_path(&panel, "root/dir1/file2", cx);
3879    assert_eq!(
3880        visible_entries_as_strings(&panel, 0..10, cx),
3881        &[
3882            "v root",
3883            "    v dir1",
3884            "          file2  <== selected",
3885            "      .gitignore",
3886            "      aa"
3887        ],
3888        "Initial state should hide files on .gitignore"
3889    );
3890    submit_deletion(&panel, cx);
3891
3892    // Test 2: Auto selection should go to the parent
3893    assert_eq!(
3894        visible_entries_as_strings(&panel, 0..10, cx),
3895        &[
3896            "v root",
3897            "    v dir1  <== selected",
3898            "      .gitignore",
3899            "      aa"
3900        ],
3901        "Initial state should hide files on .gitignore"
3902    );
3903}
3904
3905#[gpui::test]
3906async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
3907    init_test_with_editor(cx);
3908
3909    let fs = FakeFs::new(cx.executor().clone());
3910    fs.insert_tree(
3911        "/root",
3912        json!({
3913            "dir1": {
3914                "subdir1": {
3915                    "a.txt": "",
3916                    "b.txt": ""
3917                },
3918                "file1.txt": "",
3919            },
3920            "dir2": {
3921                "subdir2": {
3922                    "c.txt": "",
3923                    "d.txt": ""
3924                },
3925                "file2.txt": "",
3926            },
3927            "file3.txt": "",
3928        }),
3929    )
3930    .await;
3931
3932    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3933    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3934    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3935    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3936
3937    toggle_expand_dir(&panel, "root/dir1", cx);
3938    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
3939    toggle_expand_dir(&panel, "root/dir2", cx);
3940    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
3941
3942    // Test Case 1: Select and delete nested directory with parent
3943    cx.simulate_modifiers_change(gpui::Modifiers {
3944        control: true,
3945        ..Default::default()
3946    });
3947    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
3948    select_path_with_mark(&panel, "root/dir1", cx);
3949
3950    assert_eq!(
3951        visible_entries_as_strings(&panel, 0..15, cx),
3952        &[
3953            "v root",
3954            "    v dir1  <== selected  <== marked",
3955            "        v subdir1  <== marked",
3956            "              a.txt",
3957            "              b.txt",
3958            "          file1.txt",
3959            "    v dir2",
3960            "        v subdir2",
3961            "              c.txt",
3962            "              d.txt",
3963            "          file2.txt",
3964            "      file3.txt",
3965        ],
3966        "Initial state before deleting nested directory with parent"
3967    );
3968
3969    submit_deletion(&panel, cx);
3970    assert_eq!(
3971        visible_entries_as_strings(&panel, 0..15, cx),
3972        &[
3973            "v root",
3974            "    v dir2  <== selected",
3975            "        v subdir2",
3976            "              c.txt",
3977            "              d.txt",
3978            "          file2.txt",
3979            "      file3.txt",
3980        ],
3981        "Should select next directory after deleting directory with parent"
3982    );
3983
3984    // Test Case 2: Select mixed files and directories across levels
3985    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
3986    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
3987    select_path_with_mark(&panel, "root/file3.txt", cx);
3988
3989    assert_eq!(
3990        visible_entries_as_strings(&panel, 0..15, cx),
3991        &[
3992            "v root",
3993            "    v dir2",
3994            "        v subdir2",
3995            "              c.txt  <== marked",
3996            "              d.txt",
3997            "          file2.txt  <== marked",
3998            "      file3.txt  <== selected  <== marked",
3999        ],
4000        "Initial state before deleting"
4001    );
4002
4003    submit_deletion(&panel, cx);
4004    assert_eq!(
4005        visible_entries_as_strings(&panel, 0..15, cx),
4006        &[
4007            "v root",
4008            "    v dir2  <== selected",
4009            "        v subdir2",
4010            "              d.txt",
4011        ],
4012        "Should select sibling directory"
4013    );
4014}
4015
4016#[gpui::test]
4017async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4018    init_test_with_editor(cx);
4019
4020    let fs = FakeFs::new(cx.executor().clone());
4021    fs.insert_tree(
4022        "/root",
4023        json!({
4024            "dir1": {
4025                "subdir1": {
4026                    "a.txt": "",
4027                    "b.txt": ""
4028                },
4029                "file1.txt": "",
4030            },
4031            "dir2": {
4032                "subdir2": {
4033                    "c.txt": "",
4034                    "d.txt": ""
4035                },
4036                "file2.txt": "",
4037            },
4038            "file3.txt": "",
4039            "file4.txt": "",
4040        }),
4041    )
4042    .await;
4043
4044    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4045    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4046    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4047    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4048
4049    toggle_expand_dir(&panel, "root/dir1", cx);
4050    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4051    toggle_expand_dir(&panel, "root/dir2", cx);
4052    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4053
4054    // Test Case 1: Select all root files and directories
4055    cx.simulate_modifiers_change(gpui::Modifiers {
4056        control: true,
4057        ..Default::default()
4058    });
4059    select_path_with_mark(&panel, "root/dir1", cx);
4060    select_path_with_mark(&panel, "root/dir2", cx);
4061    select_path_with_mark(&panel, "root/file3.txt", cx);
4062    select_path_with_mark(&panel, "root/file4.txt", cx);
4063    assert_eq!(
4064        visible_entries_as_strings(&panel, 0..20, cx),
4065        &[
4066            "v root",
4067            "    v dir1  <== marked",
4068            "        v subdir1",
4069            "              a.txt",
4070            "              b.txt",
4071            "          file1.txt",
4072            "    v dir2  <== marked",
4073            "        v subdir2",
4074            "              c.txt",
4075            "              d.txt",
4076            "          file2.txt",
4077            "      file3.txt  <== marked",
4078            "      file4.txt  <== selected  <== marked",
4079        ],
4080        "State before deleting all contents"
4081    );
4082
4083    submit_deletion(&panel, cx);
4084    assert_eq!(
4085        visible_entries_as_strings(&panel, 0..20, cx),
4086        &["v root  <== selected"],
4087        "Only empty root directory should remain after deleting all contents"
4088    );
4089}
4090
4091#[gpui::test]
4092async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4093    init_test_with_editor(cx);
4094
4095    let fs = FakeFs::new(cx.executor().clone());
4096    fs.insert_tree(
4097        "/root",
4098        json!({
4099            "dir1": {
4100                "subdir1": {
4101                    "file_a.txt": "content a",
4102                    "file_b.txt": "content b",
4103                },
4104                "subdir2": {
4105                    "file_c.txt": "content c",
4106                },
4107                "file1.txt": "content 1",
4108            },
4109            "dir2": {
4110                "file2.txt": "content 2",
4111            },
4112        }),
4113    )
4114    .await;
4115
4116    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4117    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4118    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4119    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4120
4121    toggle_expand_dir(&panel, "root/dir1", cx);
4122    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4123    toggle_expand_dir(&panel, "root/dir2", cx);
4124    cx.simulate_modifiers_change(gpui::Modifiers {
4125        control: true,
4126        ..Default::default()
4127    });
4128
4129    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4130    select_path_with_mark(&panel, "root/dir1", cx);
4131    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4132    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4133
4134    assert_eq!(
4135        visible_entries_as_strings(&panel, 0..20, cx),
4136        &[
4137            "v root",
4138            "    v dir1  <== marked",
4139            "        v subdir1  <== marked",
4140            "              file_a.txt  <== selected  <== marked",
4141            "              file_b.txt",
4142            "        > subdir2",
4143            "          file1.txt",
4144            "    v dir2",
4145            "          file2.txt",
4146        ],
4147        "State with parent dir, subdir, and file selected"
4148    );
4149    submit_deletion(&panel, cx);
4150    assert_eq!(
4151        visible_entries_as_strings(&panel, 0..20, cx),
4152        &["v root", "    v dir2  <== selected", "          file2.txt",],
4153        "Only dir2 should remain after deletion"
4154    );
4155}
4156
4157#[gpui::test]
4158async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4159    init_test_with_editor(cx);
4160
4161    let fs = FakeFs::new(cx.executor().clone());
4162    // First worktree
4163    fs.insert_tree(
4164        "/root1",
4165        json!({
4166            "dir1": {
4167                "file1.txt": "content 1",
4168                "file2.txt": "content 2",
4169            },
4170            "dir2": {
4171                "file3.txt": "content 3",
4172            },
4173        }),
4174    )
4175    .await;
4176
4177    // Second worktree
4178    fs.insert_tree(
4179        "/root2",
4180        json!({
4181            "dir3": {
4182                "file4.txt": "content 4",
4183                "file5.txt": "content 5",
4184            },
4185            "file6.txt": "content 6",
4186        }),
4187    )
4188    .await;
4189
4190    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4191    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4192    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4193    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4194
4195    // Expand all directories for testing
4196    toggle_expand_dir(&panel, "root1/dir1", cx);
4197    toggle_expand_dir(&panel, "root1/dir2", cx);
4198    toggle_expand_dir(&panel, "root2/dir3", cx);
4199
4200    // Test Case 1: Delete files across different worktrees
4201    cx.simulate_modifiers_change(gpui::Modifiers {
4202        control: true,
4203        ..Default::default()
4204    });
4205    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4206    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4207
4208    assert_eq!(
4209        visible_entries_as_strings(&panel, 0..20, cx),
4210        &[
4211            "v root1",
4212            "    v dir1",
4213            "          file1.txt  <== marked",
4214            "          file2.txt",
4215            "    v dir2",
4216            "          file3.txt",
4217            "v root2",
4218            "    v dir3",
4219            "          file4.txt  <== selected  <== marked",
4220            "          file5.txt",
4221            "      file6.txt",
4222        ],
4223        "Initial state with files selected from different worktrees"
4224    );
4225
4226    submit_deletion(&panel, cx);
4227    assert_eq!(
4228        visible_entries_as_strings(&panel, 0..20, cx),
4229        &[
4230            "v root1",
4231            "    v dir1",
4232            "          file2.txt",
4233            "    v dir2",
4234            "          file3.txt",
4235            "v root2",
4236            "    v dir3",
4237            "          file5.txt  <== selected",
4238            "      file6.txt",
4239        ],
4240        "Should select next file in the last worktree after deletion"
4241    );
4242
4243    // Test Case 2: Delete directories from different worktrees
4244    select_path_with_mark(&panel, "root1/dir1", cx);
4245    select_path_with_mark(&panel, "root2/dir3", cx);
4246
4247    assert_eq!(
4248        visible_entries_as_strings(&panel, 0..20, cx),
4249        &[
4250            "v root1",
4251            "    v dir1  <== marked",
4252            "          file2.txt",
4253            "    v dir2",
4254            "          file3.txt",
4255            "v root2",
4256            "    v dir3  <== selected  <== marked",
4257            "          file5.txt",
4258            "      file6.txt",
4259        ],
4260        "State with directories marked from different worktrees"
4261    );
4262
4263    submit_deletion(&panel, cx);
4264    assert_eq!(
4265        visible_entries_as_strings(&panel, 0..20, cx),
4266        &[
4267            "v root1",
4268            "    v dir2",
4269            "          file3.txt",
4270            "v root2",
4271            "      file6.txt  <== selected",
4272        ],
4273        "Should select remaining file in last worktree after directory deletion"
4274    );
4275
4276    // Test Case 4: Delete all remaining files except roots
4277    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4278    select_path_with_mark(&panel, "root2/file6.txt", cx);
4279
4280    assert_eq!(
4281        visible_entries_as_strings(&panel, 0..20, cx),
4282        &[
4283            "v root1",
4284            "    v dir2",
4285            "          file3.txt  <== marked",
4286            "v root2",
4287            "      file6.txt  <== selected  <== marked",
4288        ],
4289        "State with all remaining files marked"
4290    );
4291
4292    submit_deletion(&panel, cx);
4293    assert_eq!(
4294        visible_entries_as_strings(&panel, 0..20, cx),
4295        &["v root1", "    v dir2", "v root2  <== selected"],
4296        "Second parent root should be selected after deleting"
4297    );
4298}
4299
4300#[gpui::test]
4301async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4302    init_test_with_editor(cx);
4303
4304    let fs = FakeFs::new(cx.executor().clone());
4305    fs.insert_tree(
4306        "/root",
4307        json!({
4308            "dir1": {
4309                "file1.txt": "",
4310                "file2.txt": "",
4311                "file3.txt": "",
4312            },
4313            "dir2": {
4314                "file4.txt": "",
4315                "file5.txt": "",
4316            },
4317        }),
4318    )
4319    .await;
4320
4321    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4322    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4323    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4324    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4325
4326    toggle_expand_dir(&panel, "root/dir1", cx);
4327    toggle_expand_dir(&panel, "root/dir2", cx);
4328
4329    cx.simulate_modifiers_change(gpui::Modifiers {
4330        control: true,
4331        ..Default::default()
4332    });
4333
4334    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4335    select_path(&panel, "root/dir1/file1.txt", cx);
4336
4337    assert_eq!(
4338        visible_entries_as_strings(&panel, 0..15, cx),
4339        &[
4340            "v root",
4341            "    v dir1",
4342            "          file1.txt  <== selected",
4343            "          file2.txt  <== marked",
4344            "          file3.txt",
4345            "    v dir2",
4346            "          file4.txt",
4347            "          file5.txt",
4348        ],
4349        "Initial state with one marked entry and different selection"
4350    );
4351
4352    // Delete should operate on the selected entry (file1.txt)
4353    submit_deletion(&panel, cx);
4354    assert_eq!(
4355        visible_entries_as_strings(&panel, 0..15, cx),
4356        &[
4357            "v root",
4358            "    v dir1",
4359            "          file2.txt  <== selected  <== marked",
4360            "          file3.txt",
4361            "    v dir2",
4362            "          file4.txt",
4363            "          file5.txt",
4364        ],
4365        "Should delete selected file, not marked file"
4366    );
4367
4368    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4369    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4370    select_path(&panel, "root/dir2/file5.txt", cx);
4371
4372    assert_eq!(
4373        visible_entries_as_strings(&panel, 0..15, cx),
4374        &[
4375            "v root",
4376            "    v dir1",
4377            "          file2.txt  <== marked",
4378            "          file3.txt  <== marked",
4379            "    v dir2",
4380            "          file4.txt  <== marked",
4381            "          file5.txt  <== selected",
4382        ],
4383        "Initial state with multiple marked entries and different selection"
4384    );
4385
4386    // Delete should operate on all marked entries, ignoring the selection
4387    submit_deletion(&panel, cx);
4388    assert_eq!(
4389        visible_entries_as_strings(&panel, 0..15, cx),
4390        &[
4391            "v root",
4392            "    v dir1",
4393            "    v dir2",
4394            "          file5.txt  <== selected",
4395        ],
4396        "Should delete all marked files, leaving only the selected file"
4397    );
4398}
4399
4400#[gpui::test]
4401async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4402    init_test_with_editor(cx);
4403
4404    let fs = FakeFs::new(cx.executor().clone());
4405    fs.insert_tree(
4406        "/root_b",
4407        json!({
4408            "dir1": {
4409                "file1.txt": "content 1",
4410                "file2.txt": "content 2",
4411            },
4412        }),
4413    )
4414    .await;
4415
4416    fs.insert_tree(
4417        "/root_c",
4418        json!({
4419            "dir2": {},
4420        }),
4421    )
4422    .await;
4423
4424    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4425    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4426    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4427    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4428
4429    toggle_expand_dir(&panel, "root_b/dir1", cx);
4430    toggle_expand_dir(&panel, "root_c/dir2", cx);
4431
4432    cx.simulate_modifiers_change(gpui::Modifiers {
4433        control: true,
4434        ..Default::default()
4435    });
4436    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4437    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4438
4439    assert_eq!(
4440        visible_entries_as_strings(&panel, 0..20, cx),
4441        &[
4442            "v root_b",
4443            "    v dir1",
4444            "          file1.txt  <== marked",
4445            "          file2.txt  <== selected  <== marked",
4446            "v root_c",
4447            "    v dir2",
4448        ],
4449        "Initial state with files marked in root_b"
4450    );
4451
4452    submit_deletion(&panel, cx);
4453    assert_eq!(
4454        visible_entries_as_strings(&panel, 0..20, cx),
4455        &[
4456            "v root_b",
4457            "    v dir1  <== selected",
4458            "v root_c",
4459            "    v dir2",
4460        ],
4461        "After deletion in root_b as it's last deletion, selection should be in root_b"
4462    );
4463
4464    select_path_with_mark(&panel, "root_c/dir2", cx);
4465
4466    submit_deletion(&panel, cx);
4467    assert_eq!(
4468        visible_entries_as_strings(&panel, 0..20, cx),
4469        &["v root_b", "    v dir1", "v root_c  <== selected",],
4470        "After deleting from root_c, it should remain in root_c"
4471    );
4472}
4473
4474fn toggle_expand_dir(
4475    panel: &Entity<ProjectPanel>,
4476    path: impl AsRef<Path>,
4477    cx: &mut VisualTestContext,
4478) {
4479    let path = path.as_ref();
4480    panel.update_in(cx, |panel, window, cx| {
4481        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4482            let worktree = worktree.read(cx);
4483            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4484                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4485                panel.toggle_expanded(entry_id, window, cx);
4486                return;
4487            }
4488        }
4489        panic!("no worktree for path {:?}", path);
4490    });
4491}
4492
4493#[gpui::test]
4494async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4495    init_test_with_editor(cx);
4496
4497    let fs = FakeFs::new(cx.executor().clone());
4498    fs.insert_tree(
4499        path!("/root"),
4500        json!({
4501            ".gitignore": "**/ignored_dir\n**/ignored_nested",
4502            "dir1": {
4503                "empty1": {
4504                    "empty2": {
4505                        "empty3": {
4506                            "file.txt": ""
4507                        }
4508                    }
4509                },
4510                "subdir1": {
4511                    "file1.txt": "",
4512                    "file2.txt": "",
4513                    "ignored_nested": {
4514                        "ignored_file.txt": ""
4515                    }
4516                },
4517                "ignored_dir": {
4518                    "subdir": {
4519                        "deep_file.txt": ""
4520                    }
4521                }
4522            }
4523        }),
4524    )
4525    .await;
4526
4527    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4528    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4529    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4530
4531    // Test 1: When auto-fold is enabled
4532    cx.update(|_, cx| {
4533        let settings = *ProjectPanelSettings::get_global(cx);
4534        ProjectPanelSettings::override_global(
4535            ProjectPanelSettings {
4536                auto_fold_dirs: true,
4537                ..settings
4538            },
4539            cx,
4540        );
4541    });
4542
4543    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4544
4545    assert_eq!(
4546        visible_entries_as_strings(&panel, 0..20, cx),
4547        &["v root", "    > dir1", "      .gitignore",],
4548        "Initial state should show collapsed root structure"
4549    );
4550
4551    toggle_expand_dir(&panel, "root/dir1", cx);
4552    assert_eq!(
4553        visible_entries_as_strings(&panel, 0..20, cx),
4554        &[
4555            separator!("v root"),
4556            separator!("    v dir1  <== selected"),
4557            separator!("        > empty1/empty2/empty3"),
4558            separator!("        > ignored_dir"),
4559            separator!("        > subdir1"),
4560            separator!("      .gitignore"),
4561        ],
4562        "Should show first level with auto-folded dirs and ignored dir visible"
4563    );
4564
4565    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4566    panel.update(cx, |panel, cx| {
4567        let project = panel.project.read(cx);
4568        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4569        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4570        panel.update_visible_entries(None, cx);
4571    });
4572    cx.run_until_parked();
4573
4574    assert_eq!(
4575        visible_entries_as_strings(&panel, 0..20, cx),
4576        &[
4577            separator!("v root"),
4578            separator!("    v dir1  <== selected"),
4579            separator!("        v empty1"),
4580            separator!("            v empty2"),
4581            separator!("                v empty3"),
4582            separator!("                      file.txt"),
4583            separator!("        > ignored_dir"),
4584            separator!("        v subdir1"),
4585            separator!("            > ignored_nested"),
4586            separator!("              file1.txt"),
4587            separator!("              file2.txt"),
4588            separator!("      .gitignore"),
4589        ],
4590        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4591    );
4592
4593    // Test 2: When auto-fold is disabled
4594    cx.update(|_, cx| {
4595        let settings = *ProjectPanelSettings::get_global(cx);
4596        ProjectPanelSettings::override_global(
4597            ProjectPanelSettings {
4598                auto_fold_dirs: false,
4599                ..settings
4600            },
4601            cx,
4602        );
4603    });
4604
4605    panel.update_in(cx, |panel, window, cx| {
4606        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4607    });
4608
4609    toggle_expand_dir(&panel, "root/dir1", cx);
4610    assert_eq!(
4611        visible_entries_as_strings(&panel, 0..20, cx),
4612        &[
4613            separator!("v root"),
4614            separator!("    v dir1  <== selected"),
4615            separator!("        > empty1"),
4616            separator!("        > ignored_dir"),
4617            separator!("        > subdir1"),
4618            separator!("      .gitignore"),
4619        ],
4620        "With auto-fold disabled: should show all directories separately"
4621    );
4622
4623    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4624    panel.update(cx, |panel, cx| {
4625        let project = panel.project.read(cx);
4626        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4627        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4628        panel.update_visible_entries(None, cx);
4629    });
4630    cx.run_until_parked();
4631
4632    assert_eq!(
4633        visible_entries_as_strings(&panel, 0..20, cx),
4634        &[
4635            separator!("v root"),
4636            separator!("    v dir1  <== selected"),
4637            separator!("        v empty1"),
4638            separator!("            v empty2"),
4639            separator!("                v empty3"),
4640            separator!("                      file.txt"),
4641            separator!("        > ignored_dir"),
4642            separator!("        v subdir1"),
4643            separator!("            > ignored_nested"),
4644            separator!("              file1.txt"),
4645            separator!("              file2.txt"),
4646            separator!("      .gitignore"),
4647        ],
4648        "After expand_all without auto-fold: should expand all dirs normally, \
4649         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4650    );
4651
4652    // Test 3: When explicitly called on ignored directory
4653    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4654    panel.update(cx, |panel, cx| {
4655        let project = panel.project.read(cx);
4656        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4657        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4658        panel.update_visible_entries(None, cx);
4659    });
4660    cx.run_until_parked();
4661
4662    assert_eq!(
4663        visible_entries_as_strings(&panel, 0..20, cx),
4664        &[
4665            separator!("v root"),
4666            separator!("    v dir1  <== selected"),
4667            separator!("        v empty1"),
4668            separator!("            v empty2"),
4669            separator!("                v empty3"),
4670            separator!("                      file.txt"),
4671            separator!("        v ignored_dir"),
4672            separator!("            v subdir"),
4673            separator!("                  deep_file.txt"),
4674            separator!("        v subdir1"),
4675            separator!("            > ignored_nested"),
4676            separator!("              file1.txt"),
4677            separator!("              file2.txt"),
4678            separator!("      .gitignore"),
4679        ],
4680        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4681    );
4682}
4683
4684#[gpui::test]
4685async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4686    init_test(cx);
4687
4688    let fs = FakeFs::new(cx.executor().clone());
4689    fs.insert_tree(
4690        path!("/root"),
4691        json!({
4692            "dir1": {
4693                "subdir1": {
4694                    "nested1": {
4695                        "file1.txt": "",
4696                        "file2.txt": ""
4697                    },
4698                },
4699                "subdir2": {
4700                    "file4.txt": ""
4701                }
4702            },
4703            "dir2": {
4704                "single_file": {
4705                    "file5.txt": ""
4706                }
4707            }
4708        }),
4709    )
4710    .await;
4711
4712    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4713    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4714    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4715
4716    // Test 1: Basic collapsing
4717    {
4718        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4719
4720        toggle_expand_dir(&panel, "root/dir1", cx);
4721        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4722        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4723        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4724
4725        assert_eq!(
4726            visible_entries_as_strings(&panel, 0..20, cx),
4727            &[
4728                separator!("v root"),
4729                separator!("    v dir1"),
4730                separator!("        v subdir1"),
4731                separator!("            v nested1"),
4732                separator!("                  file1.txt"),
4733                separator!("                  file2.txt"),
4734                separator!("        v subdir2  <== selected"),
4735                separator!("              file4.txt"),
4736                separator!("    > dir2"),
4737            ],
4738            "Initial state with everything expanded"
4739        );
4740
4741        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4742        panel.update(cx, |panel, cx| {
4743            let project = panel.project.read(cx);
4744            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4745            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4746            panel.update_visible_entries(None, cx);
4747        });
4748
4749        assert_eq!(
4750            visible_entries_as_strings(&panel, 0..20, cx),
4751            &["v root", "    > dir1", "    > dir2",],
4752            "All subdirs under dir1 should be collapsed"
4753        );
4754    }
4755
4756    // Test 2: With auto-fold enabled
4757    {
4758        cx.update(|_, cx| {
4759            let settings = *ProjectPanelSettings::get_global(cx);
4760            ProjectPanelSettings::override_global(
4761                ProjectPanelSettings {
4762                    auto_fold_dirs: true,
4763                    ..settings
4764                },
4765                cx,
4766            );
4767        });
4768
4769        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4770
4771        toggle_expand_dir(&panel, "root/dir1", cx);
4772        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4773        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4774
4775        assert_eq!(
4776            visible_entries_as_strings(&panel, 0..20, cx),
4777            &[
4778                separator!("v root"),
4779                separator!("    v dir1"),
4780                separator!("        v subdir1/nested1  <== selected"),
4781                separator!("              file1.txt"),
4782                separator!("              file2.txt"),
4783                separator!("        > subdir2"),
4784                separator!("    > dir2/single_file"),
4785            ],
4786            "Initial state with some dirs expanded"
4787        );
4788
4789        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4790        panel.update(cx, |panel, cx| {
4791            let project = panel.project.read(cx);
4792            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4793            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4794        });
4795
4796        toggle_expand_dir(&panel, "root/dir1", cx);
4797
4798        assert_eq!(
4799            visible_entries_as_strings(&panel, 0..20, cx),
4800            &[
4801                separator!("v root"),
4802                separator!("    v dir1  <== selected"),
4803                separator!("        > subdir1/nested1"),
4804                separator!("        > subdir2"),
4805                separator!("    > dir2/single_file"),
4806            ],
4807            "Subdirs should be collapsed and folded with auto-fold enabled"
4808        );
4809    }
4810
4811    // Test 3: With auto-fold disabled
4812    {
4813        cx.update(|_, cx| {
4814            let settings = *ProjectPanelSettings::get_global(cx);
4815            ProjectPanelSettings::override_global(
4816                ProjectPanelSettings {
4817                    auto_fold_dirs: false,
4818                    ..settings
4819                },
4820                cx,
4821            );
4822        });
4823
4824        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4825
4826        toggle_expand_dir(&panel, "root/dir1", cx);
4827        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4828        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4829
4830        assert_eq!(
4831            visible_entries_as_strings(&panel, 0..20, cx),
4832            &[
4833                separator!("v root"),
4834                separator!("    v dir1"),
4835                separator!("        v subdir1"),
4836                separator!("            v nested1  <== selected"),
4837                separator!("                  file1.txt"),
4838                separator!("                  file2.txt"),
4839                separator!("        > subdir2"),
4840                separator!("    > dir2"),
4841            ],
4842            "Initial state with some dirs expanded and auto-fold disabled"
4843        );
4844
4845        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4846        panel.update(cx, |panel, cx| {
4847            let project = panel.project.read(cx);
4848            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4849            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4850        });
4851
4852        toggle_expand_dir(&panel, "root/dir1", cx);
4853
4854        assert_eq!(
4855            visible_entries_as_strings(&panel, 0..20, cx),
4856            &[
4857                separator!("v root"),
4858                separator!("    v dir1  <== selected"),
4859                separator!("        > subdir1"),
4860                separator!("        > subdir2"),
4861                separator!("    > dir2"),
4862            ],
4863            "Subdirs should be collapsed but not folded with auto-fold disabled"
4864        );
4865    }
4866}
4867
4868fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
4869    let path = path.as_ref();
4870    panel.update(cx, |panel, cx| {
4871        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4872            let worktree = worktree.read(cx);
4873            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4874                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4875                panel.selection = Some(crate::SelectedEntry {
4876                    worktree_id: worktree.id(),
4877                    entry_id,
4878                });
4879                return;
4880            }
4881        }
4882        panic!("no worktree for path {:?}", path);
4883    });
4884}
4885
4886fn select_path_with_mark(
4887    panel: &Entity<ProjectPanel>,
4888    path: impl AsRef<Path>,
4889    cx: &mut VisualTestContext,
4890) {
4891    let path = path.as_ref();
4892    panel.update(cx, |panel, cx| {
4893        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4894            let worktree = worktree.read(cx);
4895            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4896                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4897                let entry = crate::SelectedEntry {
4898                    worktree_id: worktree.id(),
4899                    entry_id,
4900                };
4901                if !panel.marked_entries.contains(&entry) {
4902                    panel.marked_entries.insert(entry);
4903                }
4904                panel.selection = Some(entry);
4905                return;
4906            }
4907        }
4908        panic!("no worktree for path {:?}", path);
4909    });
4910}
4911
4912fn find_project_entry(
4913    panel: &Entity<ProjectPanel>,
4914    path: impl AsRef<Path>,
4915    cx: &mut VisualTestContext,
4916) -> Option<ProjectEntryId> {
4917    let path = path.as_ref();
4918    panel.update(cx, |panel, cx| {
4919        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4920            let worktree = worktree.read(cx);
4921            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4922                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
4923            }
4924        }
4925        panic!("no worktree for path {path:?}");
4926    })
4927}
4928
4929fn visible_entries_as_strings(
4930    panel: &Entity<ProjectPanel>,
4931    range: Range<usize>,
4932    cx: &mut VisualTestContext,
4933) -> Vec<String> {
4934    let mut result = Vec::new();
4935    let mut project_entries = HashSet::default();
4936    let mut has_editor = false;
4937
4938    panel.update_in(cx, |panel, window, cx| {
4939        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
4940            if details.is_editing {
4941                assert!(!has_editor, "duplicate editor entry");
4942                has_editor = true;
4943            } else {
4944                assert!(
4945                    project_entries.insert(project_entry),
4946                    "duplicate project entry {:?} {:?}",
4947                    project_entry,
4948                    details
4949                );
4950            }
4951
4952            let indent = "    ".repeat(details.depth);
4953            let icon = if details.kind.is_dir() {
4954                if details.is_expanded { "v " } else { "> " }
4955            } else {
4956                "  "
4957            };
4958            let name = if details.is_editing {
4959                format!("[EDITOR: '{}']", details.filename)
4960            } else if details.is_processing {
4961                format!("[PROCESSING: '{}']", details.filename)
4962            } else {
4963                details.filename.clone()
4964            };
4965            let selected = if details.is_selected {
4966                "  <== selected"
4967            } else {
4968                ""
4969            };
4970            let marked = if details.is_marked {
4971                "  <== marked"
4972            } else {
4973                ""
4974            };
4975
4976            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
4977        });
4978    });
4979
4980    result
4981}
4982
4983fn init_test(cx: &mut TestAppContext) {
4984    cx.update(|cx| {
4985        let settings_store = SettingsStore::test(cx);
4986        cx.set_global(settings_store);
4987        init_settings(cx);
4988        theme::init(theme::LoadThemes::JustBase, cx);
4989        language::init(cx);
4990        editor::init_settings(cx);
4991        crate::init(cx);
4992        workspace::init_settings(cx);
4993        client::init_settings(cx);
4994        Project::init_settings(cx);
4995
4996        cx.update_global::<SettingsStore, _>(|store, cx| {
4997            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
4998                project_panel_settings.auto_fold_dirs = Some(false);
4999            });
5000            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5001                worktree_settings.file_scan_exclusions = Some(Vec::new());
5002            });
5003        });
5004    });
5005}
5006
5007fn init_test_with_editor(cx: &mut TestAppContext) {
5008    cx.update(|cx| {
5009        let app_state = AppState::test(cx);
5010        theme::init(theme::LoadThemes::JustBase, cx);
5011        init_settings(cx);
5012        language::init(cx);
5013        editor::init(cx);
5014        crate::init(cx);
5015        workspace::init(app_state.clone(), cx);
5016        Project::init_settings(cx);
5017
5018        cx.update_global::<SettingsStore, _>(|store, cx| {
5019            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5020                project_panel_settings.auto_fold_dirs = Some(false);
5021            });
5022            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5023                worktree_settings.file_scan_exclusions = Some(Vec::new());
5024            });
5025        });
5026    });
5027}
5028
5029fn ensure_single_file_is_opened(
5030    window: &WindowHandle<Workspace>,
5031    expected_path: &str,
5032    cx: &mut TestAppContext,
5033) {
5034    window
5035        .update(cx, |workspace, _, cx| {
5036            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5037            assert_eq!(worktrees.len(), 1);
5038            let worktree_id = worktrees[0].read(cx).id();
5039
5040            let open_project_paths = workspace
5041                .panes()
5042                .iter()
5043                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5044                .collect::<Vec<_>>();
5045            assert_eq!(
5046                open_project_paths,
5047                vec![ProjectPath {
5048                    worktree_id,
5049                    path: Arc::from(Path::new(expected_path))
5050                }],
5051                "Should have opened file, selected in project panel"
5052            );
5053        })
5054        .unwrap();
5055}
5056
5057fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5058    assert!(
5059        !cx.has_pending_prompt(),
5060        "Should have no prompts before the deletion"
5061    );
5062    panel.update_in(cx, |panel, window, cx| {
5063        panel.delete(&Delete { skip_prompt: false }, window, cx)
5064    });
5065    assert!(
5066        cx.has_pending_prompt(),
5067        "Should have a prompt after the deletion"
5068    );
5069    cx.simulate_prompt_answer("Delete");
5070    assert!(
5071        !cx.has_pending_prompt(),
5072        "Should have no prompts after prompt was replied to"
5073    );
5074    cx.executor().run_until_parked();
5075}
5076
5077fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5078    assert!(
5079        !cx.has_pending_prompt(),
5080        "Should have no prompts before the deletion"
5081    );
5082    panel.update_in(cx, |panel, window, cx| {
5083        panel.delete(&Delete { skip_prompt: true }, window, cx)
5084    });
5085    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5086    cx.executor().run_until_parked();
5087}
5088
5089fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5090    assert!(
5091        !cx.has_pending_prompt(),
5092        "Should have no prompts after deletion operation closes the file"
5093    );
5094    workspace
5095        .read_with(cx, |workspace, cx| {
5096            let open_project_paths = workspace
5097                .panes()
5098                .iter()
5099                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5100                .collect::<Vec<_>>();
5101            assert!(
5102                open_project_paths.is_empty(),
5103                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5104            );
5105        })
5106        .unwrap();
5107}
5108
5109struct TestProjectItemView {
5110    focus_handle: FocusHandle,
5111    path: ProjectPath,
5112}
5113
5114struct TestProjectItem {
5115    path: ProjectPath,
5116}
5117
5118impl project::ProjectItem for TestProjectItem {
5119    fn try_open(
5120        _project: &Entity<Project>,
5121        path: &ProjectPath,
5122        cx: &mut App,
5123    ) -> Option<Task<gpui::Result<Entity<Self>>>> {
5124        let path = path.clone();
5125        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5126    }
5127
5128    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5129        None
5130    }
5131
5132    fn project_path(&self, _: &App) -> Option<ProjectPath> {
5133        Some(self.path.clone())
5134    }
5135
5136    fn is_dirty(&self) -> bool {
5137        false
5138    }
5139}
5140
5141impl ProjectItem for TestProjectItemView {
5142    type Item = TestProjectItem;
5143
5144    fn for_project_item(
5145        _: Entity<Project>,
5146        _: &Pane,
5147        project_item: Entity<Self::Item>,
5148        _: &mut Window,
5149        cx: &mut Context<Self>,
5150    ) -> Self
5151    where
5152        Self: Sized,
5153    {
5154        Self {
5155            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5156            focus_handle: cx.focus_handle(),
5157        }
5158    }
5159}
5160
5161impl Item for TestProjectItemView {
5162    type Event = ();
5163}
5164
5165impl EventEmitter<()> for TestProjectItemView {}
5166
5167impl Focusable for TestProjectItemView {
5168    fn focus_handle(&self, _: &App) -> FocusHandle {
5169        self.focus_handle.clone()
5170    }
5171}
5172
5173impl Render for TestProjectItemView {
5174    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5175        Empty
5176    }
5177}