project_panel_tests.rs

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