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