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(cx: &mut gpui::TestAppContext) {
1175    init_test(cx);
1176
1177    let fs = FakeFs::new(cx.executor().clone());
1178    fs.insert_tree(
1179        "/root",
1180        json!({
1181            "one.txt": "",
1182            "two.txt": "",
1183            "a": {},
1184            "b": {}
1185        }),
1186    )
1187    .await;
1188
1189    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1190    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1191    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1192    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1193
1194    select_path_with_mark(&panel, "root/one.txt", cx);
1195    select_path_with_mark(&panel, "root/two.txt", cx);
1196
1197    assert_eq!(
1198        visible_entries_as_strings(&panel, 0..50, cx),
1199        &[
1200            "v root",
1201            "    > a",
1202            "    > b",
1203            "      one.txt  <== marked",
1204            "      two.txt  <== selected  <== marked",
1205        ]
1206    );
1207
1208    panel.update_in(cx, |panel, window, cx| {
1209        panel.cut(&Default::default(), window, cx);
1210    });
1211
1212    select_path(&panel, "root/a", cx);
1213
1214    panel.update_in(cx, |panel, window, cx| {
1215        panel.paste(&Default::default(), window, cx);
1216    });
1217    cx.executor().run_until_parked();
1218
1219    assert_eq!(
1220        visible_entries_as_strings(&panel, 0..50, cx),
1221        &[
1222            "v root",
1223            "    v a",
1224            "          one.txt  <== marked",
1225            "          two.txt  <== selected  <== marked",
1226            "    > b",
1227        ],
1228        "Cut entries should be moved on first paste."
1229    );
1230
1231    panel.update_in(cx, |panel, window, cx| {
1232        panel.cancel(&menu::Cancel {}, window, cx)
1233    });
1234    cx.executor().run_until_parked();
1235
1236    select_path(&panel, "root/b", cx);
1237
1238    panel.update_in(cx, |panel, window, cx| {
1239        panel.paste(&Default::default(), window, cx);
1240    });
1241    cx.executor().run_until_parked();
1242
1243    assert_eq!(
1244        visible_entries_as_strings(&panel, 0..50, cx),
1245        &[
1246            "v root",
1247            "    v a",
1248            "          one.txt",
1249            "          two.txt",
1250            "    v b",
1251            "          one.txt",
1252            "          two.txt  <== selected",
1253        ],
1254        "Cut entries should only be copied for the second paste!"
1255    );
1256}
1257
1258#[gpui::test]
1259async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1260    init_test(cx);
1261
1262    let fs = FakeFs::new(cx.executor().clone());
1263    fs.insert_tree(
1264        "/root1",
1265        json!({
1266            "one.txt": "",
1267            "two.txt": "",
1268            "three.txt": "",
1269            "a": {
1270                "0": { "q": "", "r": "", "s": "" },
1271                "1": { "t": "", "u": "" },
1272                "2": { "v": "", "w": "", "x": "", "y": "" },
1273            },
1274        }),
1275    )
1276    .await;
1277
1278    fs.insert_tree(
1279        "/root2",
1280        json!({
1281            "one.txt": "",
1282            "two.txt": "",
1283            "four.txt": "",
1284            "b": {
1285                "3": { "Q": "" },
1286                "4": { "R": "", "S": "", "T": "", "U": "" },
1287            },
1288        }),
1289    )
1290    .await;
1291
1292    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1293    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1294    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1295    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1296
1297    select_path(&panel, "root1/three.txt", cx);
1298    panel.update_in(cx, |panel, window, cx| {
1299        panel.cut(&Default::default(), window, cx);
1300    });
1301
1302    select_path(&panel, "root2/one.txt", cx);
1303    panel.update_in(cx, |panel, window, cx| {
1304        panel.select_next(&Default::default(), window, cx);
1305        panel.paste(&Default::default(), window, cx);
1306    });
1307    cx.executor().run_until_parked();
1308    assert_eq!(
1309        visible_entries_as_strings(&panel, 0..50, cx),
1310        &[
1311            //
1312            "v root1",
1313            "    > a",
1314            "      one.txt",
1315            "      two.txt",
1316            "v root2",
1317            "    > b",
1318            "      four.txt",
1319            "      one.txt",
1320            "      three.txt  <== selected  <== marked",
1321            "      two.txt",
1322        ]
1323    );
1324
1325    select_path(&panel, "root1/a", cx);
1326    panel.update_in(cx, |panel, window, cx| {
1327        panel.cut(&Default::default(), window, cx);
1328    });
1329    select_path(&panel, "root2/two.txt", cx);
1330    panel.update_in(cx, |panel, window, cx| {
1331        panel.select_next(&Default::default(), window, cx);
1332        panel.paste(&Default::default(), window, cx);
1333    });
1334
1335    cx.executor().run_until_parked();
1336    assert_eq!(
1337        visible_entries_as_strings(&panel, 0..50, cx),
1338        &[
1339            //
1340            "v root1",
1341            "      one.txt",
1342            "      two.txt",
1343            "v root2",
1344            "    > a  <== selected",
1345            "    > b",
1346            "      four.txt",
1347            "      one.txt",
1348            "      three.txt  <== marked",
1349            "      two.txt",
1350        ]
1351    );
1352}
1353
1354#[gpui::test]
1355async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1356    init_test(cx);
1357
1358    let fs = FakeFs::new(cx.executor().clone());
1359    fs.insert_tree(
1360        "/root1",
1361        json!({
1362            "one.txt": "",
1363            "two.txt": "",
1364            "three.txt": "",
1365            "a": {
1366                "0": { "q": "", "r": "", "s": "" },
1367                "1": { "t": "", "u": "" },
1368                "2": { "v": "", "w": "", "x": "", "y": "" },
1369            },
1370        }),
1371    )
1372    .await;
1373
1374    fs.insert_tree(
1375        "/root2",
1376        json!({
1377            "one.txt": "",
1378            "two.txt": "",
1379            "four.txt": "",
1380            "b": {
1381                "3": { "Q": "" },
1382                "4": { "R": "", "S": "", "T": "", "U": "" },
1383            },
1384        }),
1385    )
1386    .await;
1387
1388    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1389    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1390    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1391    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1392
1393    select_path(&panel, "root1/three.txt", cx);
1394    panel.update_in(cx, |panel, window, cx| {
1395        panel.copy(&Default::default(), window, cx);
1396    });
1397
1398    select_path(&panel, "root2/one.txt", cx);
1399    panel.update_in(cx, |panel, window, cx| {
1400        panel.select_next(&Default::default(), window, cx);
1401        panel.paste(&Default::default(), window, cx);
1402    });
1403    cx.executor().run_until_parked();
1404    assert_eq!(
1405        visible_entries_as_strings(&panel, 0..50, cx),
1406        &[
1407            //
1408            "v root1",
1409            "    > a",
1410            "      one.txt",
1411            "      three.txt",
1412            "      two.txt",
1413            "v root2",
1414            "    > b",
1415            "      four.txt",
1416            "      one.txt",
1417            "      three.txt  <== selected  <== marked",
1418            "      two.txt",
1419        ]
1420    );
1421
1422    select_path(&panel, "root1/three.txt", cx);
1423    panel.update_in(cx, |panel, window, cx| {
1424        panel.copy(&Default::default(), window, cx);
1425    });
1426    select_path(&panel, "root2/two.txt", cx);
1427    panel.update_in(cx, |panel, window, cx| {
1428        panel.select_next(&Default::default(), window, cx);
1429        panel.paste(&Default::default(), window, cx);
1430    });
1431
1432    cx.executor().run_until_parked();
1433    assert_eq!(
1434        visible_entries_as_strings(&panel, 0..50, cx),
1435        &[
1436            //
1437            "v root1",
1438            "    > a",
1439            "      one.txt",
1440            "      three.txt",
1441            "      two.txt",
1442            "v root2",
1443            "    > b",
1444            "      four.txt",
1445            "      one.txt",
1446            "      three.txt",
1447            "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
1448            "      two.txt",
1449        ]
1450    );
1451
1452    panel.update_in(cx, |panel, window, cx| {
1453        panel.cancel(&menu::Cancel {}, window, cx)
1454    });
1455    cx.executor().run_until_parked();
1456
1457    select_path(&panel, "root1/a", cx);
1458    panel.update_in(cx, |panel, window, cx| {
1459        panel.copy(&Default::default(), window, cx);
1460    });
1461    select_path(&panel, "root2/two.txt", cx);
1462    panel.update_in(cx, |panel, window, cx| {
1463        panel.select_next(&Default::default(), window, cx);
1464        panel.paste(&Default::default(), window, cx);
1465    });
1466
1467    cx.executor().run_until_parked();
1468    assert_eq!(
1469        visible_entries_as_strings(&panel, 0..50, cx),
1470        &[
1471            //
1472            "v root1",
1473            "    > a",
1474            "      one.txt",
1475            "      three.txt",
1476            "      two.txt",
1477            "v root2",
1478            "    > a  <== selected",
1479            "    > b",
1480            "      four.txt",
1481            "      one.txt",
1482            "      three.txt",
1483            "      three copy.txt",
1484            "      two.txt",
1485        ]
1486    );
1487}
1488
1489#[gpui::test]
1490async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1491    init_test(cx);
1492
1493    let fs = FakeFs::new(cx.executor().clone());
1494    fs.insert_tree(
1495        "/root",
1496        json!({
1497            "a": {
1498                "one.txt": "",
1499                "two.txt": "",
1500                "inner_dir": {
1501                    "three.txt": "",
1502                    "four.txt": "",
1503                }
1504            },
1505            "b": {}
1506        }),
1507    )
1508    .await;
1509
1510    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1511    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1512    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1513    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1514
1515    select_path(&panel, "root/a", cx);
1516    panel.update_in(cx, |panel, window, cx| {
1517        panel.copy(&Default::default(), window, cx);
1518        panel.select_next(&Default::default(), window, cx);
1519        panel.paste(&Default::default(), window, cx);
1520    });
1521    cx.executor().run_until_parked();
1522
1523    let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1524    assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1525
1526    let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1527    assert_ne!(
1528        pasted_dir_file, None,
1529        "Pasted directory file should have an entry"
1530    );
1531
1532    let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1533    assert_ne!(
1534        pasted_dir_inner_dir, None,
1535        "Directories inside pasted directory should have an entry"
1536    );
1537
1538    toggle_expand_dir(&panel, "root/b/a", cx);
1539    toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1540
1541    assert_eq!(
1542        visible_entries_as_strings(&panel, 0..50, cx),
1543        &[
1544            //
1545            "v root",
1546            "    > a",
1547            "    v b",
1548            "        v a",
1549            "            v inner_dir  <== selected",
1550            "                  four.txt",
1551            "                  three.txt",
1552            "              one.txt",
1553            "              two.txt",
1554        ]
1555    );
1556
1557    select_path(&panel, "root", cx);
1558    panel.update_in(cx, |panel, window, cx| {
1559        panel.paste(&Default::default(), window, cx)
1560    });
1561    cx.executor().run_until_parked();
1562    assert_eq!(
1563        visible_entries_as_strings(&panel, 0..50, cx),
1564        &[
1565            //
1566            "v root",
1567            "    > a",
1568            "    > [EDITOR: 'a copy']  <== selected",
1569            "    v b",
1570            "        v a",
1571            "            v inner_dir",
1572            "                  four.txt",
1573            "                  three.txt",
1574            "              one.txt",
1575            "              two.txt"
1576        ]
1577    );
1578
1579    let confirm = panel.update_in(cx, |panel, window, cx| {
1580        panel
1581            .filename_editor
1582            .update(cx, |editor, cx| editor.set_text("c", window, cx));
1583        panel.confirm_edit(window, cx).unwrap()
1584    });
1585    assert_eq!(
1586        visible_entries_as_strings(&panel, 0..50, cx),
1587        &[
1588            //
1589            "v root",
1590            "    > a",
1591            "    > [PROCESSING: 'c']  <== selected",
1592            "    v b",
1593            "        v a",
1594            "            v inner_dir",
1595            "                  four.txt",
1596            "                  three.txt",
1597            "              one.txt",
1598            "              two.txt"
1599        ]
1600    );
1601
1602    confirm.await.unwrap();
1603
1604    panel.update_in(cx, |panel, window, cx| {
1605        panel.paste(&Default::default(), window, cx)
1606    });
1607    cx.executor().run_until_parked();
1608    assert_eq!(
1609        visible_entries_as_strings(&panel, 0..50, cx),
1610        &[
1611            //
1612            "v root",
1613            "    > a",
1614            "    v b",
1615            "        v a",
1616            "            v inner_dir",
1617            "                  four.txt",
1618            "                  three.txt",
1619            "              one.txt",
1620            "              two.txt",
1621            "    v c",
1622            "        > a  <== selected",
1623            "        > inner_dir",
1624            "          one.txt",
1625            "          two.txt",
1626        ]
1627    );
1628}
1629
1630#[gpui::test]
1631async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1632    init_test(cx);
1633
1634    let fs = FakeFs::new(cx.executor().clone());
1635    fs.insert_tree(
1636        "/test",
1637        json!({
1638            "dir1": {
1639                "a.txt": "",
1640                "b.txt": "",
1641            },
1642            "dir2": {},
1643            "c.txt": "",
1644            "d.txt": "",
1645        }),
1646    )
1647    .await;
1648
1649    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1650    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1651    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1652    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1653
1654    toggle_expand_dir(&panel, "test/dir1", cx);
1655
1656    cx.simulate_modifiers_change(gpui::Modifiers {
1657        control: true,
1658        ..Default::default()
1659    });
1660
1661    select_path_with_mark(&panel, "test/dir1", cx);
1662    select_path_with_mark(&panel, "test/c.txt", cx);
1663
1664    assert_eq!(
1665        visible_entries_as_strings(&panel, 0..15, cx),
1666        &[
1667            "v test",
1668            "    v dir1  <== marked",
1669            "          a.txt",
1670            "          b.txt",
1671            "    > dir2",
1672            "      c.txt  <== selected  <== marked",
1673            "      d.txt",
1674        ],
1675        "Initial state before copying dir1 and c.txt"
1676    );
1677
1678    panel.update_in(cx, |panel, window, cx| {
1679        panel.copy(&Default::default(), window, cx);
1680    });
1681    select_path(&panel, "test/dir2", cx);
1682    panel.update_in(cx, |panel, window, cx| {
1683        panel.paste(&Default::default(), window, cx);
1684    });
1685    cx.executor().run_until_parked();
1686
1687    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1688
1689    assert_eq!(
1690        visible_entries_as_strings(&panel, 0..15, cx),
1691        &[
1692            "v test",
1693            "    v dir1  <== marked",
1694            "          a.txt",
1695            "          b.txt",
1696            "    v dir2",
1697            "        v dir1  <== selected",
1698            "              a.txt",
1699            "              b.txt",
1700            "          c.txt",
1701            "      c.txt  <== marked",
1702            "      d.txt",
1703        ],
1704        "Should copy dir1 as well as c.txt into dir2"
1705    );
1706
1707    // Disambiguating multiple files should not open the rename editor.
1708    select_path(&panel, "test/dir2", cx);
1709    panel.update_in(cx, |panel, window, cx| {
1710        panel.paste(&Default::default(), window, cx);
1711    });
1712    cx.executor().run_until_parked();
1713
1714    assert_eq!(
1715        visible_entries_as_strings(&panel, 0..15, cx),
1716        &[
1717            "v test",
1718            "    v dir1  <== marked",
1719            "          a.txt",
1720            "          b.txt",
1721            "    v dir2",
1722            "        v dir1",
1723            "              a.txt",
1724            "              b.txt",
1725            "        > dir1 copy  <== selected",
1726            "          c.txt",
1727            "          c copy.txt",
1728            "      c.txt  <== marked",
1729            "      d.txt",
1730        ],
1731        "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1732    );
1733}
1734
1735#[gpui::test]
1736async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1737    init_test(cx);
1738
1739    let fs = FakeFs::new(cx.executor().clone());
1740    fs.insert_tree(
1741        "/test",
1742        json!({
1743            "dir1": {
1744                "a.txt": "",
1745                "b.txt": "",
1746            },
1747            "dir2": {},
1748            "c.txt": "",
1749            "d.txt": "",
1750        }),
1751    )
1752    .await;
1753
1754    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1755    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1756    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1757    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1758
1759    toggle_expand_dir(&panel, "test/dir1", cx);
1760
1761    cx.simulate_modifiers_change(gpui::Modifiers {
1762        control: true,
1763        ..Default::default()
1764    });
1765
1766    select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1767    select_path_with_mark(&panel, "test/dir1", cx);
1768    select_path_with_mark(&panel, "test/c.txt", cx);
1769
1770    assert_eq!(
1771        visible_entries_as_strings(&panel, 0..15, cx),
1772        &[
1773            "v test",
1774            "    v dir1  <== marked",
1775            "          a.txt  <== marked",
1776            "          b.txt",
1777            "    > dir2",
1778            "      c.txt  <== selected  <== marked",
1779            "      d.txt",
1780        ],
1781        "Initial state before copying a.txt, dir1 and c.txt"
1782    );
1783
1784    panel.update_in(cx, |panel, window, cx| {
1785        panel.copy(&Default::default(), window, cx);
1786    });
1787    select_path(&panel, "test/dir2", cx);
1788    panel.update_in(cx, |panel, window, cx| {
1789        panel.paste(&Default::default(), window, cx);
1790    });
1791    cx.executor().run_until_parked();
1792
1793    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1794
1795    assert_eq!(
1796        visible_entries_as_strings(&panel, 0..20, cx),
1797        &[
1798            "v test",
1799            "    v dir1  <== marked",
1800            "          a.txt  <== marked",
1801            "          b.txt",
1802            "    v dir2",
1803            "        v dir1  <== selected",
1804            "              a.txt",
1805            "              b.txt",
1806            "          c.txt",
1807            "      c.txt  <== marked",
1808            "      d.txt",
1809        ],
1810        "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1811    );
1812}
1813
1814#[gpui::test]
1815async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1816    init_test_with_editor(cx);
1817
1818    let fs = FakeFs::new(cx.executor().clone());
1819    fs.insert_tree(
1820        path!("/src"),
1821        json!({
1822            "test": {
1823                "first.rs": "// First Rust file",
1824                "second.rs": "// Second Rust file",
1825                "third.rs": "// Third Rust file",
1826            }
1827        }),
1828    )
1829    .await;
1830
1831    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1832    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1833    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1834    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1835
1836    toggle_expand_dir(&panel, "src/test", cx);
1837    select_path(&panel, "src/test/first.rs", cx);
1838    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1839    cx.executor().run_until_parked();
1840    assert_eq!(
1841        visible_entries_as_strings(&panel, 0..10, cx),
1842        &[
1843            "v src",
1844            "    v test",
1845            "          first.rs  <== selected  <== marked",
1846            "          second.rs",
1847            "          third.rs"
1848        ]
1849    );
1850    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1851
1852    submit_deletion(&panel, cx);
1853    assert_eq!(
1854        visible_entries_as_strings(&panel, 0..10, cx),
1855        &[
1856            "v src",
1857            "    v test",
1858            "          second.rs  <== selected",
1859            "          third.rs"
1860        ],
1861        "Project panel should have no deleted file, no other file is selected in it"
1862    );
1863    ensure_no_open_items_and_panes(&workspace, cx);
1864
1865    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1866    cx.executor().run_until_parked();
1867    assert_eq!(
1868        visible_entries_as_strings(&panel, 0..10, cx),
1869        &[
1870            "v src",
1871            "    v test",
1872            "          second.rs  <== selected  <== marked",
1873            "          third.rs"
1874        ]
1875    );
1876    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1877
1878    workspace
1879        .update(cx, |workspace, window, cx| {
1880            let active_items = workspace
1881                .panes()
1882                .iter()
1883                .filter_map(|pane| pane.read(cx).active_item())
1884                .collect::<Vec<_>>();
1885            assert_eq!(active_items.len(), 1);
1886            let open_editor = active_items
1887                .into_iter()
1888                .next()
1889                .unwrap()
1890                .downcast::<Editor>()
1891                .expect("Open item should be an editor");
1892            open_editor.update(cx, |editor, cx| {
1893                editor.set_text("Another text!", window, cx)
1894            });
1895        })
1896        .unwrap();
1897    submit_deletion_skipping_prompt(&panel, cx);
1898    assert_eq!(
1899        visible_entries_as_strings(&panel, 0..10, cx),
1900        &["v src", "    v test", "          third.rs  <== selected"],
1901        "Project panel should have no deleted file, with one last file remaining"
1902    );
1903    ensure_no_open_items_and_panes(&workspace, cx);
1904}
1905
1906#[gpui::test]
1907async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
1908    init_test_with_editor(cx);
1909
1910    let fs = FakeFs::new(cx.executor().clone());
1911    fs.insert_tree(
1912        "/src",
1913        json!({
1914            "test": {
1915                "first.rs": "// First Rust file",
1916                "second.rs": "// Second Rust file",
1917                "third.rs": "// Third Rust file",
1918            }
1919        }),
1920    )
1921    .await;
1922
1923    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
1924    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1925    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1926    let panel = workspace
1927        .update(cx, |workspace, window, cx| {
1928            let panel = ProjectPanel::new(workspace, window, cx);
1929            workspace.add_panel(panel.clone(), window, cx);
1930            panel
1931        })
1932        .unwrap();
1933
1934    select_path(&panel, "src/", cx);
1935    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1936    cx.executor().run_until_parked();
1937    assert_eq!(
1938        visible_entries_as_strings(&panel, 0..10, cx),
1939        &[
1940            //
1941            "v src  <== selected",
1942            "    > test"
1943        ]
1944    );
1945    panel.update_in(cx, |panel, window, cx| {
1946        panel.new_directory(&NewDirectory, window, cx)
1947    });
1948    panel.update_in(cx, |panel, window, cx| {
1949        assert!(panel.filename_editor.read(cx).is_focused(window));
1950    });
1951    assert_eq!(
1952        visible_entries_as_strings(&panel, 0..10, cx),
1953        &[
1954            //
1955            "v src",
1956            "    > [EDITOR: '']  <== selected",
1957            "    > test"
1958        ]
1959    );
1960    panel.update_in(cx, |panel, window, cx| {
1961        panel
1962            .filename_editor
1963            .update(cx, |editor, cx| editor.set_text("test", window, cx));
1964        assert!(
1965            panel.confirm_edit(window, cx).is_none(),
1966            "Should not allow to confirm on conflicting new directory name"
1967        );
1968    });
1969    cx.executor().run_until_parked();
1970    panel.update_in(cx, |panel, window, cx| {
1971        assert!(
1972            panel.edit_state.is_some(),
1973            "Edit state should not be None after conflicting new directory name"
1974        );
1975        panel.cancel(&menu::Cancel, window, cx);
1976    });
1977    assert_eq!(
1978        visible_entries_as_strings(&panel, 0..10, cx),
1979        &[
1980            //
1981            "v src  <== selected",
1982            "    > test"
1983        ],
1984        "File list should be unchanged after failed folder create confirmation"
1985    );
1986
1987    select_path(&panel, "src/test/", cx);
1988    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1989    cx.executor().run_until_parked();
1990    assert_eq!(
1991        visible_entries_as_strings(&panel, 0..10, cx),
1992        &[
1993            //
1994            "v src",
1995            "    > test  <== selected"
1996        ]
1997    );
1998    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1999    panel.update_in(cx, |panel, window, cx| {
2000        assert!(panel.filename_editor.read(cx).is_focused(window));
2001    });
2002    assert_eq!(
2003        visible_entries_as_strings(&panel, 0..10, cx),
2004        &[
2005            "v src",
2006            "    v test",
2007            "          [EDITOR: '']  <== selected",
2008            "          first.rs",
2009            "          second.rs",
2010            "          third.rs"
2011        ]
2012    );
2013    panel.update_in(cx, |panel, window, cx| {
2014        panel
2015            .filename_editor
2016            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2017        assert!(
2018            panel.confirm_edit(window, cx).is_none(),
2019            "Should not allow to confirm on conflicting new file name"
2020        );
2021    });
2022    cx.executor().run_until_parked();
2023    panel.update_in(cx, |panel, window, cx| {
2024        assert!(
2025            panel.edit_state.is_some(),
2026            "Edit state should not be None after conflicting new file name"
2027        );
2028        panel.cancel(&menu::Cancel, window, cx);
2029    });
2030    assert_eq!(
2031        visible_entries_as_strings(&panel, 0..10, cx),
2032        &[
2033            "v src",
2034            "    v test  <== selected",
2035            "          first.rs",
2036            "          second.rs",
2037            "          third.rs"
2038        ],
2039        "File list should be unchanged after failed file create confirmation"
2040    );
2041
2042    select_path(&panel, "src/test/first.rs", cx);
2043    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2044    cx.executor().run_until_parked();
2045    assert_eq!(
2046        visible_entries_as_strings(&panel, 0..10, cx),
2047        &[
2048            "v src",
2049            "    v test",
2050            "          first.rs  <== selected",
2051            "          second.rs",
2052            "          third.rs"
2053        ],
2054    );
2055    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2056    panel.update_in(cx, |panel, window, cx| {
2057        assert!(panel.filename_editor.read(cx).is_focused(window));
2058    });
2059    assert_eq!(
2060        visible_entries_as_strings(&panel, 0..10, cx),
2061        &[
2062            "v src",
2063            "    v test",
2064            "          [EDITOR: 'first.rs']  <== selected",
2065            "          second.rs",
2066            "          third.rs"
2067        ]
2068    );
2069    panel.update_in(cx, |panel, window, cx| {
2070        panel
2071            .filename_editor
2072            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2073        assert!(
2074            panel.confirm_edit(window, cx).is_none(),
2075            "Should not allow to confirm on conflicting file rename"
2076        )
2077    });
2078    cx.executor().run_until_parked();
2079    panel.update_in(cx, |panel, window, cx| {
2080        assert!(
2081            panel.edit_state.is_some(),
2082            "Edit state should not be None after conflicting file rename"
2083        );
2084        panel.cancel(&menu::Cancel, window, cx);
2085    });
2086    assert_eq!(
2087        visible_entries_as_strings(&panel, 0..10, cx),
2088        &[
2089            "v src",
2090            "    v test",
2091            "          first.rs  <== selected",
2092            "          second.rs",
2093            "          third.rs"
2094        ],
2095        "File list should be unchanged after failed rename confirmation"
2096    );
2097}
2098
2099#[gpui::test]
2100async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2101    init_test_with_editor(cx);
2102
2103    let fs = FakeFs::new(cx.executor().clone());
2104    fs.insert_tree(
2105        path!("/root"),
2106        json!({
2107            "tree1": {
2108                ".git": {},
2109                "dir1": {
2110                    "modified1.txt": "1",
2111                    "unmodified1.txt": "1",
2112                    "modified2.txt": "1",
2113                },
2114                "dir2": {
2115                    "modified3.txt": "1",
2116                    "unmodified2.txt": "1",
2117                },
2118                "modified4.txt": "1",
2119                "unmodified3.txt": "1",
2120            },
2121            "tree2": {
2122                ".git": {},
2123                "dir3": {
2124                    "modified5.txt": "1",
2125                    "unmodified4.txt": "1",
2126                },
2127                "modified6.txt": "1",
2128                "unmodified5.txt": "1",
2129            }
2130        }),
2131    )
2132    .await;
2133
2134    // Mark files as git modified
2135    fs.set_git_content_for_repo(
2136        path!("/root/tree1/.git").as_ref(),
2137        &[
2138            ("dir1/modified1.txt".into(), "modified".into(), None),
2139            ("dir1/modified2.txt".into(), "modified".into(), None),
2140            ("modified4.txt".into(), "modified".into(), None),
2141            ("dir2/modified3.txt".into(), "modified".into(), None),
2142        ],
2143    );
2144    fs.set_git_content_for_repo(
2145        path!("/root/tree2/.git").as_ref(),
2146        &[
2147            ("dir3/modified5.txt".into(), "modified".into(), None),
2148            ("modified6.txt".into(), "modified".into(), None),
2149        ],
2150    );
2151
2152    let project = Project::test(
2153        fs.clone(),
2154        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2155        cx,
2156    )
2157    .await;
2158
2159    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2160        let mut worktrees = project.worktrees(cx);
2161        let worktree1 = worktrees.next().unwrap();
2162        let worktree2 = worktrees.next().unwrap();
2163        (
2164            worktree1.read(cx).as_local().unwrap().scan_complete(),
2165            worktree2.read(cx).as_local().unwrap().scan_complete(),
2166        )
2167    });
2168    scan1_complete.await;
2169    scan2_complete.await;
2170    cx.run_until_parked();
2171
2172    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2173    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2174    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2175
2176    // Check initial state
2177    assert_eq!(
2178        visible_entries_as_strings(&panel, 0..15, cx),
2179        &[
2180            "v tree1",
2181            "    > .git",
2182            "    > dir1",
2183            "    > dir2",
2184            "      modified4.txt",
2185            "      unmodified3.txt",
2186            "v tree2",
2187            "    > .git",
2188            "    > dir3",
2189            "      modified6.txt",
2190            "      unmodified5.txt"
2191        ],
2192    );
2193
2194    // Test selecting next modified entry
2195    panel.update_in(cx, |panel, window, cx| {
2196        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2197    });
2198
2199    assert_eq!(
2200        visible_entries_as_strings(&panel, 0..6, cx),
2201        &[
2202            "v tree1",
2203            "    > .git",
2204            "    v dir1",
2205            "          modified1.txt  <== selected",
2206            "          modified2.txt",
2207            "          unmodified1.txt",
2208        ],
2209    );
2210
2211    panel.update_in(cx, |panel, window, cx| {
2212        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2213    });
2214
2215    assert_eq!(
2216        visible_entries_as_strings(&panel, 0..6, cx),
2217        &[
2218            "v tree1",
2219            "    > .git",
2220            "    v dir1",
2221            "          modified1.txt",
2222            "          modified2.txt  <== selected",
2223            "          unmodified1.txt",
2224        ],
2225    );
2226
2227    panel.update_in(cx, |panel, window, cx| {
2228        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2229    });
2230
2231    assert_eq!(
2232        visible_entries_as_strings(&panel, 6..9, cx),
2233        &[
2234            "    v dir2",
2235            "          modified3.txt  <== selected",
2236            "          unmodified2.txt",
2237        ],
2238    );
2239
2240    panel.update_in(cx, |panel, window, cx| {
2241        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2242    });
2243
2244    assert_eq!(
2245        visible_entries_as_strings(&panel, 9..11, cx),
2246        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2247    );
2248
2249    panel.update_in(cx, |panel, window, cx| {
2250        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2251    });
2252
2253    assert_eq!(
2254        visible_entries_as_strings(&panel, 13..16, cx),
2255        &[
2256            "    v dir3",
2257            "          modified5.txt  <== selected",
2258            "          unmodified4.txt",
2259        ],
2260    );
2261
2262    panel.update_in(cx, |panel, window, cx| {
2263        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2264    });
2265
2266    assert_eq!(
2267        visible_entries_as_strings(&panel, 16..18, cx),
2268        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2269    );
2270
2271    // Wraps around to first modified file
2272    panel.update_in(cx, |panel, window, cx| {
2273        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2274    });
2275
2276    assert_eq!(
2277        visible_entries_as_strings(&panel, 0..18, cx),
2278        &[
2279            "v tree1",
2280            "    > .git",
2281            "    v dir1",
2282            "          modified1.txt  <== selected",
2283            "          modified2.txt",
2284            "          unmodified1.txt",
2285            "    v dir2",
2286            "          modified3.txt",
2287            "          unmodified2.txt",
2288            "      modified4.txt",
2289            "      unmodified3.txt",
2290            "v tree2",
2291            "    > .git",
2292            "    v dir3",
2293            "          modified5.txt",
2294            "          unmodified4.txt",
2295            "      modified6.txt",
2296            "      unmodified5.txt",
2297        ],
2298    );
2299
2300    // Wraps around again to last modified file
2301    panel.update_in(cx, |panel, window, cx| {
2302        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2303    });
2304
2305    assert_eq!(
2306        visible_entries_as_strings(&panel, 16..18, cx),
2307        &["      modified6.txt  <== selected", "      unmodified5.txt",],
2308    );
2309
2310    panel.update_in(cx, |panel, window, cx| {
2311        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2312    });
2313
2314    assert_eq!(
2315        visible_entries_as_strings(&panel, 13..16, cx),
2316        &[
2317            "    v dir3",
2318            "          modified5.txt  <== selected",
2319            "          unmodified4.txt",
2320        ],
2321    );
2322
2323    panel.update_in(cx, |panel, window, cx| {
2324        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2325    });
2326
2327    assert_eq!(
2328        visible_entries_as_strings(&panel, 9..11, cx),
2329        &["      modified4.txt  <== selected", "      unmodified3.txt",],
2330    );
2331
2332    panel.update_in(cx, |panel, window, cx| {
2333        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2334    });
2335
2336    assert_eq!(
2337        visible_entries_as_strings(&panel, 6..9, cx),
2338        &[
2339            "    v dir2",
2340            "          modified3.txt  <== selected",
2341            "          unmodified2.txt",
2342        ],
2343    );
2344
2345    panel.update_in(cx, |panel, window, cx| {
2346        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2347    });
2348
2349    assert_eq!(
2350        visible_entries_as_strings(&panel, 0..6, cx),
2351        &[
2352            "v tree1",
2353            "    > .git",
2354            "    v dir1",
2355            "          modified1.txt",
2356            "          modified2.txt  <== selected",
2357            "          unmodified1.txt",
2358        ],
2359    );
2360
2361    panel.update_in(cx, |panel, window, cx| {
2362        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2363    });
2364
2365    assert_eq!(
2366        visible_entries_as_strings(&panel, 0..6, cx),
2367        &[
2368            "v tree1",
2369            "    > .git",
2370            "    v dir1",
2371            "          modified1.txt  <== selected",
2372            "          modified2.txt",
2373            "          unmodified1.txt",
2374        ],
2375    );
2376}
2377
2378#[gpui::test]
2379async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2380    init_test_with_editor(cx);
2381
2382    let fs = FakeFs::new(cx.executor().clone());
2383    fs.insert_tree(
2384        "/project_root",
2385        json!({
2386            "dir_1": {
2387                "nested_dir": {
2388                    "file_a.py": "# File contents",
2389                }
2390            },
2391            "file_1.py": "# File contents",
2392            "dir_2": {
2393
2394            },
2395            "dir_3": {
2396
2397            },
2398            "file_2.py": "# File contents",
2399            "dir_4": {
2400
2401            },
2402        }),
2403    )
2404    .await;
2405
2406    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2407    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2408    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2409    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2410
2411    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2412    cx.executor().run_until_parked();
2413    select_path(&panel, "project_root/dir_1", cx);
2414    cx.executor().run_until_parked();
2415    assert_eq!(
2416        visible_entries_as_strings(&panel, 0..10, cx),
2417        &[
2418            "v project_root",
2419            "    > dir_1  <== selected",
2420            "    > dir_2",
2421            "    > dir_3",
2422            "    > dir_4",
2423            "      file_1.py",
2424            "      file_2.py",
2425        ]
2426    );
2427    panel.update_in(cx, |panel, window, cx| {
2428        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2429    });
2430
2431    assert_eq!(
2432        visible_entries_as_strings(&panel, 0..10, cx),
2433        &[
2434            "v project_root  <== selected",
2435            "    > dir_1",
2436            "    > dir_2",
2437            "    > dir_3",
2438            "    > dir_4",
2439            "      file_1.py",
2440            "      file_2.py",
2441        ]
2442    );
2443
2444    panel.update_in(cx, |panel, window, cx| {
2445        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2446    });
2447
2448    assert_eq!(
2449        visible_entries_as_strings(&panel, 0..10, cx),
2450        &[
2451            "v project_root",
2452            "    > dir_1",
2453            "    > dir_2",
2454            "    > dir_3",
2455            "    > dir_4  <== selected",
2456            "      file_1.py",
2457            "      file_2.py",
2458        ]
2459    );
2460
2461    panel.update_in(cx, |panel, window, cx| {
2462        panel.select_next_directory(&SelectNextDirectory, window, cx)
2463    });
2464
2465    assert_eq!(
2466        visible_entries_as_strings(&panel, 0..10, cx),
2467        &[
2468            "v project_root  <== selected",
2469            "    > dir_1",
2470            "    > dir_2",
2471            "    > dir_3",
2472            "    > dir_4",
2473            "      file_1.py",
2474            "      file_2.py",
2475        ]
2476    );
2477}
2478#[gpui::test]
2479async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2480    init_test_with_editor(cx);
2481
2482    let fs = FakeFs::new(cx.executor().clone());
2483    fs.insert_tree(
2484        "/project_root",
2485        json!({
2486            "dir_1": {
2487                "nested_dir": {
2488                    "file_a.py": "# File contents",
2489                }
2490            },
2491            "file_1.py": "# File contents",
2492            "file_2.py": "# File contents",
2493            "zdir_2": {
2494                "nested_dir2": {
2495                    "file_b.py": "# File contents",
2496                }
2497            },
2498        }),
2499    )
2500    .await;
2501
2502    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2503    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2504    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2505    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2506
2507    assert_eq!(
2508        visible_entries_as_strings(&panel, 0..10, cx),
2509        &[
2510            "v project_root",
2511            "    > dir_1",
2512            "    > zdir_2",
2513            "      file_1.py",
2514            "      file_2.py",
2515        ]
2516    );
2517    panel.update_in(cx, |panel, window, cx| {
2518        panel.select_first(&SelectFirst, window, cx)
2519    });
2520
2521    assert_eq!(
2522        visible_entries_as_strings(&panel, 0..10, cx),
2523        &[
2524            "v project_root  <== selected",
2525            "    > dir_1",
2526            "    > zdir_2",
2527            "      file_1.py",
2528            "      file_2.py",
2529        ]
2530    );
2531
2532    panel.update_in(cx, |panel, window, cx| {
2533        panel.select_last(&SelectLast, window, cx)
2534    });
2535
2536    assert_eq!(
2537        visible_entries_as_strings(&panel, 0..10, cx),
2538        &[
2539            "v project_root",
2540            "    > dir_1",
2541            "    > zdir_2",
2542            "      file_1.py",
2543            "      file_2.py  <== selected",
2544        ]
2545    );
2546}
2547
2548#[gpui::test]
2549async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2550    init_test_with_editor(cx);
2551
2552    let fs = FakeFs::new(cx.executor().clone());
2553    fs.insert_tree(
2554        "/project_root",
2555        json!({
2556            "dir_1": {
2557                "nested_dir": {
2558                    "file_a.py": "# File contents",
2559                }
2560            },
2561            "file_1.py": "# File contents",
2562        }),
2563    )
2564    .await;
2565
2566    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2567    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2568    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2569    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2570
2571    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2572    cx.executor().run_until_parked();
2573    select_path(&panel, "project_root/dir_1", cx);
2574    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2575    select_path(&panel, "project_root/dir_1/nested_dir", cx);
2576    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2577    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2578    cx.executor().run_until_parked();
2579    assert_eq!(
2580        visible_entries_as_strings(&panel, 0..10, cx),
2581        &[
2582            "v project_root",
2583            "    v dir_1",
2584            "        > nested_dir  <== selected",
2585            "      file_1.py",
2586        ]
2587    );
2588}
2589
2590#[gpui::test]
2591async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2592    init_test_with_editor(cx);
2593
2594    let fs = FakeFs::new(cx.executor().clone());
2595    fs.insert_tree(
2596        "/project_root",
2597        json!({
2598            "dir_1": {
2599                "nested_dir": {
2600                    "file_a.py": "# File contents",
2601                    "file_b.py": "# File contents",
2602                    "file_c.py": "# File contents",
2603                },
2604                "file_1.py": "# File contents",
2605                "file_2.py": "# File contents",
2606                "file_3.py": "# File contents",
2607            },
2608            "dir_2": {
2609                "file_1.py": "# File contents",
2610                "file_2.py": "# File contents",
2611                "file_3.py": "# File contents",
2612            }
2613        }),
2614    )
2615    .await;
2616
2617    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2618    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2619    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2620    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2621
2622    panel.update_in(cx, |panel, window, cx| {
2623        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2624    });
2625    cx.executor().run_until_parked();
2626    assert_eq!(
2627        visible_entries_as_strings(&panel, 0..10, cx),
2628        &["v project_root", "    > dir_1", "    > dir_2",]
2629    );
2630
2631    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2632    toggle_expand_dir(&panel, "project_root/dir_1", cx);
2633    cx.executor().run_until_parked();
2634    assert_eq!(
2635        visible_entries_as_strings(&panel, 0..10, cx),
2636        &[
2637            "v project_root",
2638            "    v dir_1  <== selected",
2639            "        > nested_dir",
2640            "          file_1.py",
2641            "          file_2.py",
2642            "          file_3.py",
2643            "    > dir_2",
2644        ]
2645    );
2646}
2647
2648#[gpui::test]
2649async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2650    init_test(cx);
2651
2652    let fs = FakeFs::new(cx.executor().clone());
2653    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2654    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2655    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2656    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2657    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2658
2659    // Make a new buffer with no backing file
2660    workspace
2661        .update(cx, |workspace, window, cx| {
2662            Editor::new_file(workspace, &Default::default(), window, cx)
2663        })
2664        .unwrap();
2665
2666    cx.executor().run_until_parked();
2667
2668    // "Save as" the buffer, creating a new backing file for it
2669    let save_task = workspace
2670        .update(cx, |workspace, window, cx| {
2671            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2672        })
2673        .unwrap();
2674
2675    cx.executor().run_until_parked();
2676    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2677    save_task.await.unwrap();
2678
2679    // Rename the file
2680    select_path(&panel, "root/new", cx);
2681    assert_eq!(
2682        visible_entries_as_strings(&panel, 0..10, cx),
2683        &["v root", "      new  <== selected  <== marked"]
2684    );
2685    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2686    panel.update_in(cx, |panel, window, cx| {
2687        panel
2688            .filename_editor
2689            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2690    });
2691    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2692
2693    cx.executor().run_until_parked();
2694    assert_eq!(
2695        visible_entries_as_strings(&panel, 0..10, cx),
2696        &["v root", "      newer  <== selected"]
2697    );
2698
2699    workspace
2700        .update(cx, |workspace, window, cx| {
2701            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2702        })
2703        .unwrap()
2704        .await
2705        .unwrap();
2706
2707    cx.executor().run_until_parked();
2708    // assert that saving the file doesn't restore "new"
2709    assert_eq!(
2710        visible_entries_as_strings(&panel, 0..10, cx),
2711        &["v root", "      newer  <== selected"]
2712    );
2713}
2714
2715#[gpui::test]
2716#[cfg_attr(target_os = "windows", ignore)]
2717async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2718    init_test_with_editor(cx);
2719
2720    let fs = FakeFs::new(cx.executor().clone());
2721    fs.insert_tree(
2722        "/root1",
2723        json!({
2724            "dir1": {
2725                "file1.txt": "content 1",
2726            },
2727        }),
2728    )
2729    .await;
2730
2731    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2732    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2733    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2734    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2735
2736    toggle_expand_dir(&panel, "root1/dir1", cx);
2737
2738    assert_eq!(
2739        visible_entries_as_strings(&panel, 0..20, cx),
2740        &["v root1", "    v dir1  <== selected", "          file1.txt",],
2741        "Initial state with worktrees"
2742    );
2743
2744    select_path(&panel, "root1", cx);
2745    assert_eq!(
2746        visible_entries_as_strings(&panel, 0..20, cx),
2747        &["v root1  <== selected", "    v dir1", "          file1.txt",],
2748    );
2749
2750    // Rename root1 to new_root1
2751    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2752
2753    assert_eq!(
2754        visible_entries_as_strings(&panel, 0..20, cx),
2755        &[
2756            "v [EDITOR: 'root1']  <== selected",
2757            "    v dir1",
2758            "          file1.txt",
2759        ],
2760    );
2761
2762    let confirm = panel.update_in(cx, |panel, window, cx| {
2763        panel
2764            .filename_editor
2765            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2766        panel.confirm_edit(window, cx).unwrap()
2767    });
2768    confirm.await.unwrap();
2769    assert_eq!(
2770        visible_entries_as_strings(&panel, 0..20, cx),
2771        &[
2772            "v new_root1  <== selected",
2773            "    v dir1",
2774            "          file1.txt",
2775        ],
2776        "Should update worktree name"
2777    );
2778
2779    // Ensure internal paths have been updated
2780    select_path(&panel, "new_root1/dir1/file1.txt", cx);
2781    assert_eq!(
2782        visible_entries_as_strings(&panel, 0..20, cx),
2783        &[
2784            "v new_root1",
2785            "    v dir1",
2786            "          file1.txt  <== selected",
2787        ],
2788        "Files in renamed worktree are selectable"
2789    );
2790}
2791
2792#[gpui::test]
2793async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2794    init_test_with_editor(cx);
2795    let fs = FakeFs::new(cx.executor().clone());
2796    fs.insert_tree(
2797        "/project_root",
2798        json!({
2799            "dir_1": {
2800                "nested_dir": {
2801                    "file_a.py": "# File contents",
2802                }
2803            },
2804            "file_1.py": "# File contents",
2805        }),
2806    )
2807    .await;
2808
2809    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2810    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2811    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2812    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2813    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2814    cx.update(|window, cx| {
2815        panel.update(cx, |this, cx| {
2816            this.select_next(&Default::default(), window, cx);
2817            this.expand_selected_entry(&Default::default(), window, cx);
2818            this.expand_selected_entry(&Default::default(), window, cx);
2819            this.select_next(&Default::default(), window, cx);
2820            this.expand_selected_entry(&Default::default(), window, cx);
2821            this.select_next(&Default::default(), window, cx);
2822        })
2823    });
2824    assert_eq!(
2825        visible_entries_as_strings(&panel, 0..10, cx),
2826        &[
2827            "v project_root",
2828            "    v dir_1",
2829            "        v nested_dir",
2830            "              file_a.py  <== selected",
2831            "      file_1.py",
2832        ]
2833    );
2834    let modifiers_with_shift = gpui::Modifiers {
2835        shift: true,
2836        ..Default::default()
2837    };
2838    cx.run_until_parked();
2839    cx.simulate_modifiers_change(modifiers_with_shift);
2840    cx.update(|window, cx| {
2841        panel.update(cx, |this, cx| {
2842            this.select_next(&Default::default(), window, cx);
2843        })
2844    });
2845    assert_eq!(
2846        visible_entries_as_strings(&panel, 0..10, cx),
2847        &[
2848            "v project_root",
2849            "    v dir_1",
2850            "        v nested_dir",
2851            "              file_a.py",
2852            "      file_1.py  <== selected  <== marked",
2853        ]
2854    );
2855    cx.update(|window, cx| {
2856        panel.update(cx, |this, cx| {
2857            this.select_previous(&Default::default(), window, cx);
2858        })
2859    });
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",
2866            "              file_a.py  <== selected  <== marked",
2867            "      file_1.py  <== marked",
2868        ]
2869    );
2870    cx.update(|window, cx| {
2871        panel.update(cx, |this, cx| {
2872            let drag = DraggedSelection {
2873                active_selection: this.selection.unwrap(),
2874                marked_selections: Arc::new(this.marked_entries.clone()),
2875            };
2876            let target_entry = this
2877                .project
2878                .read(cx)
2879                .entry_for_path(&(worktree_id, "").into(), cx)
2880                .unwrap();
2881            this.drag_onto(&drag, target_entry.id, false, window, cx);
2882        });
2883    });
2884    cx.run_until_parked();
2885    assert_eq!(
2886        visible_entries_as_strings(&panel, 0..10, cx),
2887        &[
2888            "v project_root",
2889            "    v dir_1",
2890            "        v nested_dir",
2891            "      file_1.py  <== marked",
2892            "      file_a.py  <== selected  <== marked",
2893        ]
2894    );
2895    // ESC clears out all marks
2896    cx.update(|window, cx| {
2897        panel.update(cx, |this, cx| {
2898            this.cancel(&menu::Cancel, window, cx);
2899        })
2900    });
2901    assert_eq!(
2902        visible_entries_as_strings(&panel, 0..10, cx),
2903        &[
2904            "v project_root",
2905            "    v dir_1",
2906            "        v nested_dir",
2907            "      file_1.py",
2908            "      file_a.py  <== selected",
2909        ]
2910    );
2911    // ESC clears out all marks
2912    cx.update(|window, cx| {
2913        panel.update(cx, |this, cx| {
2914            this.select_previous(&SelectPrevious, window, cx);
2915            this.select_next(&SelectNext, window, cx);
2916        })
2917    });
2918    assert_eq!(
2919        visible_entries_as_strings(&panel, 0..10, cx),
2920        &[
2921            "v project_root",
2922            "    v dir_1",
2923            "        v nested_dir",
2924            "      file_1.py  <== marked",
2925            "      file_a.py  <== selected  <== marked",
2926        ]
2927    );
2928    cx.simulate_modifiers_change(Default::default());
2929    cx.update(|window, cx| {
2930        panel.update(cx, |this, cx| {
2931            this.cut(&Cut, window, cx);
2932            this.select_previous(&SelectPrevious, window, cx);
2933            this.select_previous(&SelectPrevious, window, cx);
2934
2935            this.paste(&Paste, window, cx);
2936            // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2937        })
2938    });
2939    cx.run_until_parked();
2940    assert_eq!(
2941        visible_entries_as_strings(&panel, 0..10, cx),
2942        &[
2943            "v project_root",
2944            "    v dir_1",
2945            "        v nested_dir",
2946            "              file_1.py  <== marked",
2947            "              file_a.py  <== selected  <== marked",
2948        ]
2949    );
2950    cx.simulate_modifiers_change(modifiers_with_shift);
2951    cx.update(|window, cx| {
2952        panel.update(cx, |this, cx| {
2953            this.expand_selected_entry(&Default::default(), window, cx);
2954            this.select_next(&SelectNext, window, cx);
2955            this.select_next(&SelectNext, window, cx);
2956        })
2957    });
2958    submit_deletion(&panel, cx);
2959    assert_eq!(
2960        visible_entries_as_strings(&panel, 0..10, cx),
2961        &[
2962            "v project_root",
2963            "    v dir_1",
2964            "        v nested_dir  <== selected",
2965        ]
2966    );
2967}
2968#[gpui::test]
2969async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2970    init_test_with_editor(cx);
2971    cx.update(|cx| {
2972        cx.update_global::<SettingsStore, _>(|store, cx| {
2973            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2974                worktree_settings.file_scan_exclusions = Some(Vec::new());
2975            });
2976            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2977                project_panel_settings.auto_reveal_entries = Some(false)
2978            });
2979        })
2980    });
2981
2982    let fs = FakeFs::new(cx.background_executor.clone());
2983    fs.insert_tree(
2984        "/project_root",
2985        json!({
2986            ".git": {},
2987            ".gitignore": "**/gitignored_dir",
2988            "dir_1": {
2989                "file_1.py": "# File 1_1 contents",
2990                "file_2.py": "# File 1_2 contents",
2991                "file_3.py": "# File 1_3 contents",
2992                "gitignored_dir": {
2993                    "file_a.py": "# File contents",
2994                    "file_b.py": "# File contents",
2995                    "file_c.py": "# File contents",
2996                },
2997            },
2998            "dir_2": {
2999                "file_1.py": "# File 2_1 contents",
3000                "file_2.py": "# File 2_2 contents",
3001                "file_3.py": "# File 2_3 contents",
3002            }
3003        }),
3004    )
3005    .await;
3006
3007    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3008    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3009    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3010    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3011
3012    assert_eq!(
3013        visible_entries_as_strings(&panel, 0..20, cx),
3014        &[
3015            "v project_root",
3016            "    > .git",
3017            "    > dir_1",
3018            "    > dir_2",
3019            "      .gitignore",
3020        ]
3021    );
3022
3023    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3024        .expect("dir 1 file is not ignored and should have an entry");
3025    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3026        .expect("dir 2 file is not ignored and should have an entry");
3027    let gitignored_dir_file =
3028        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3029    assert_eq!(
3030        gitignored_dir_file, None,
3031        "File in the gitignored dir should not have an entry before its dir is toggled"
3032    );
3033
3034    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3035    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3036    cx.executor().run_until_parked();
3037    assert_eq!(
3038        visible_entries_as_strings(&panel, 0..20, cx),
3039        &[
3040            "v project_root",
3041            "    > .git",
3042            "    v dir_1",
3043            "        v gitignored_dir  <== selected",
3044            "              file_a.py",
3045            "              file_b.py",
3046            "              file_c.py",
3047            "          file_1.py",
3048            "          file_2.py",
3049            "          file_3.py",
3050            "    > dir_2",
3051            "      .gitignore",
3052        ],
3053        "Should show gitignored dir file list in the project panel"
3054    );
3055    let gitignored_dir_file =
3056        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3057            .expect("after gitignored dir got opened, a file entry should be present");
3058
3059    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3060    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3061    assert_eq!(
3062        visible_entries_as_strings(&panel, 0..20, cx),
3063        &[
3064            "v project_root",
3065            "    > .git",
3066            "    > dir_1  <== selected",
3067            "    > dir_2",
3068            "      .gitignore",
3069        ],
3070        "Should hide all dir contents again and prepare for the auto reveal test"
3071    );
3072
3073    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3074        panel.update(cx, |panel, cx| {
3075            panel.project.update(cx, |_, cx| {
3076                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3077            })
3078        });
3079        cx.run_until_parked();
3080        assert_eq!(
3081            visible_entries_as_strings(&panel, 0..20, cx),
3082            &[
3083                "v project_root",
3084                "    > .git",
3085                "    > dir_1  <== selected",
3086                "    > dir_2",
3087                "      .gitignore",
3088            ],
3089            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3090        );
3091    }
3092
3093    cx.update(|_, cx| {
3094        cx.update_global::<SettingsStore, _>(|store, cx| {
3095            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3096                project_panel_settings.auto_reveal_entries = Some(true)
3097            });
3098        })
3099    });
3100
3101    panel.update(cx, |panel, cx| {
3102        panel.project.update(cx, |_, cx| {
3103            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3104        })
3105    });
3106    cx.run_until_parked();
3107    assert_eq!(
3108        visible_entries_as_strings(&panel, 0..20, cx),
3109        &[
3110            "v project_root",
3111            "    > .git",
3112            "    v dir_1",
3113            "        > gitignored_dir",
3114            "          file_1.py  <== selected  <== marked",
3115            "          file_2.py",
3116            "          file_3.py",
3117            "    > dir_2",
3118            "      .gitignore",
3119        ],
3120        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3121    );
3122
3123    panel.update(cx, |panel, cx| {
3124        panel.project.update(cx, |_, cx| {
3125            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3126        })
3127    });
3128    cx.run_until_parked();
3129    assert_eq!(
3130        visible_entries_as_strings(&panel, 0..20, cx),
3131        &[
3132            "v project_root",
3133            "    > .git",
3134            "    v dir_1",
3135            "        > gitignored_dir",
3136            "          file_1.py",
3137            "          file_2.py",
3138            "          file_3.py",
3139            "    v dir_2",
3140            "          file_1.py  <== selected  <== marked",
3141            "          file_2.py",
3142            "          file_3.py",
3143            "      .gitignore",
3144        ],
3145        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3146    );
3147
3148    panel.update(cx, |panel, cx| {
3149        panel.project.update(cx, |_, cx| {
3150            cx.emit(project::Event::ActiveEntryChanged(Some(
3151                gitignored_dir_file,
3152            )))
3153        })
3154    });
3155    cx.run_until_parked();
3156    assert_eq!(
3157        visible_entries_as_strings(&panel, 0..20, cx),
3158        &[
3159            "v project_root",
3160            "    > .git",
3161            "    v dir_1",
3162            "        > gitignored_dir",
3163            "          file_1.py",
3164            "          file_2.py",
3165            "          file_3.py",
3166            "    v dir_2",
3167            "          file_1.py  <== selected  <== marked",
3168            "          file_2.py",
3169            "          file_3.py",
3170            "      .gitignore",
3171        ],
3172        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3173    );
3174
3175    panel.update(cx, |panel, cx| {
3176        panel.project.update(cx, |_, cx| {
3177            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3178        })
3179    });
3180    cx.run_until_parked();
3181    assert_eq!(
3182        visible_entries_as_strings(&panel, 0..20, cx),
3183        &[
3184            "v project_root",
3185            "    > .git",
3186            "    v dir_1",
3187            "        v gitignored_dir",
3188            "              file_a.py  <== selected  <== marked",
3189            "              file_b.py",
3190            "              file_c.py",
3191            "          file_1.py",
3192            "          file_2.py",
3193            "          file_3.py",
3194            "    v dir_2",
3195            "          file_1.py",
3196            "          file_2.py",
3197            "          file_3.py",
3198            "      .gitignore",
3199        ],
3200        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3201    );
3202}
3203
3204#[gpui::test]
3205async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3206    init_test_with_editor(cx);
3207    cx.update(|cx| {
3208        cx.update_global::<SettingsStore, _>(|store, cx| {
3209            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3210                worktree_settings.file_scan_exclusions = Some(Vec::new());
3211                worktree_settings.file_scan_inclusions =
3212                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3213            });
3214            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3215                project_panel_settings.auto_reveal_entries = Some(false)
3216            });
3217        })
3218    });
3219
3220    let fs = FakeFs::new(cx.background_executor.clone());
3221    fs.insert_tree(
3222        "/project_root",
3223        json!({
3224            ".git": {},
3225            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3226            "dir_1": {
3227                "file_1.py": "# File 1_1 contents",
3228                "file_2.py": "# File 1_2 contents",
3229                "file_3.py": "# File 1_3 contents",
3230                "gitignored_dir": {
3231                    "file_a.py": "# File contents",
3232                    "file_b.py": "# File contents",
3233                    "file_c.py": "# File contents",
3234                },
3235            },
3236            "dir_2": {
3237                "file_1.py": "# File 2_1 contents",
3238                "file_2.py": "# File 2_2 contents",
3239                "file_3.py": "# File 2_3 contents",
3240            },
3241            "always_included_but_ignored_dir": {
3242                "file_a.py": "# File contents",
3243                "file_b.py": "# File contents",
3244                "file_c.py": "# File contents",
3245            },
3246        }),
3247    )
3248    .await;
3249
3250    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3251    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3252    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3253    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3254
3255    assert_eq!(
3256        visible_entries_as_strings(&panel, 0..20, cx),
3257        &[
3258            "v project_root",
3259            "    > .git",
3260            "    > always_included_but_ignored_dir",
3261            "    > dir_1",
3262            "    > dir_2",
3263            "      .gitignore",
3264        ]
3265    );
3266
3267    let gitignored_dir_file =
3268        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3269    let always_included_but_ignored_dir_file = find_project_entry(
3270        &panel,
3271        "project_root/always_included_but_ignored_dir/file_a.py",
3272        cx,
3273    )
3274    .expect("file that is .gitignored but set to always be included should have an entry");
3275    assert_eq!(
3276        gitignored_dir_file, None,
3277        "File in the gitignored dir should not have an entry unless its directory is toggled"
3278    );
3279
3280    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3281    cx.run_until_parked();
3282    cx.update(|_, cx| {
3283        cx.update_global::<SettingsStore, _>(|store, cx| {
3284            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3285                project_panel_settings.auto_reveal_entries = Some(true)
3286            });
3287        })
3288    });
3289
3290    panel.update(cx, |panel, cx| {
3291        panel.project.update(cx, |_, cx| {
3292            cx.emit(project::Event::ActiveEntryChanged(Some(
3293                always_included_but_ignored_dir_file,
3294            )))
3295        })
3296    });
3297    cx.run_until_parked();
3298
3299    assert_eq!(
3300        visible_entries_as_strings(&panel, 0..20, cx),
3301        &[
3302            "v project_root",
3303            "    > .git",
3304            "    v always_included_but_ignored_dir",
3305            "          file_a.py  <== selected  <== marked",
3306            "          file_b.py",
3307            "          file_c.py",
3308            "    v dir_1",
3309            "        > gitignored_dir",
3310            "          file_1.py",
3311            "          file_2.py",
3312            "          file_3.py",
3313            "    > dir_2",
3314            "      .gitignore",
3315        ],
3316        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3317    );
3318}
3319
3320#[gpui::test]
3321async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3322    init_test_with_editor(cx);
3323    cx.update(|cx| {
3324        cx.update_global::<SettingsStore, _>(|store, cx| {
3325            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3326                worktree_settings.file_scan_exclusions = Some(Vec::new());
3327            });
3328            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3329                project_panel_settings.auto_reveal_entries = Some(false)
3330            });
3331        })
3332    });
3333
3334    let fs = FakeFs::new(cx.background_executor.clone());
3335    fs.insert_tree(
3336        "/project_root",
3337        json!({
3338            ".git": {},
3339            ".gitignore": "**/gitignored_dir",
3340            "dir_1": {
3341                "file_1.py": "# File 1_1 contents",
3342                "file_2.py": "# File 1_2 contents",
3343                "file_3.py": "# File 1_3 contents",
3344                "gitignored_dir": {
3345                    "file_a.py": "# File contents",
3346                    "file_b.py": "# File contents",
3347                    "file_c.py": "# File contents",
3348                },
3349            },
3350            "dir_2": {
3351                "file_1.py": "# File 2_1 contents",
3352                "file_2.py": "# File 2_2 contents",
3353                "file_3.py": "# File 2_3 contents",
3354            }
3355        }),
3356    )
3357    .await;
3358
3359    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3360    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3361    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3362    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3363
3364    assert_eq!(
3365        visible_entries_as_strings(&panel, 0..20, cx),
3366        &[
3367            "v project_root",
3368            "    > .git",
3369            "    > dir_1",
3370            "    > dir_2",
3371            "      .gitignore",
3372        ]
3373    );
3374
3375    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3376        .expect("dir 1 file is not ignored and should have an entry");
3377    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3378        .expect("dir 2 file is not ignored and should have an entry");
3379    let gitignored_dir_file =
3380        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3381    assert_eq!(
3382        gitignored_dir_file, None,
3383        "File in the gitignored dir should not have an entry before its dir is toggled"
3384    );
3385
3386    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3387    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3388    cx.run_until_parked();
3389    assert_eq!(
3390        visible_entries_as_strings(&panel, 0..20, cx),
3391        &[
3392            "v project_root",
3393            "    > .git",
3394            "    v dir_1",
3395            "        v gitignored_dir  <== selected",
3396            "              file_a.py",
3397            "              file_b.py",
3398            "              file_c.py",
3399            "          file_1.py",
3400            "          file_2.py",
3401            "          file_3.py",
3402            "    > dir_2",
3403            "      .gitignore",
3404        ],
3405        "Should show gitignored dir file list in the project panel"
3406    );
3407    let gitignored_dir_file =
3408        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3409            .expect("after gitignored dir got opened, a file entry should be present");
3410
3411    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3412    toggle_expand_dir(&panel, "project_root/dir_1", cx);
3413    assert_eq!(
3414        visible_entries_as_strings(&panel, 0..20, cx),
3415        &[
3416            "v project_root",
3417            "    > .git",
3418            "    > dir_1  <== selected",
3419            "    > dir_2",
3420            "      .gitignore",
3421        ],
3422        "Should hide all dir contents again and prepare for the explicit reveal test"
3423    );
3424
3425    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3426        panel.update(cx, |panel, cx| {
3427            panel.project.update(cx, |_, cx| {
3428                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3429            })
3430        });
3431        cx.run_until_parked();
3432        assert_eq!(
3433            visible_entries_as_strings(&panel, 0..20, cx),
3434            &[
3435                "v project_root",
3436                "    > .git",
3437                "    > dir_1  <== selected",
3438                "    > dir_2",
3439                "      .gitignore",
3440            ],
3441            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3442        );
3443    }
3444
3445    panel.update(cx, |panel, cx| {
3446        panel.project.update(cx, |_, cx| {
3447            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3448        })
3449    });
3450    cx.run_until_parked();
3451    assert_eq!(
3452        visible_entries_as_strings(&panel, 0..20, cx),
3453        &[
3454            "v project_root",
3455            "    > .git",
3456            "    v dir_1",
3457            "        > gitignored_dir",
3458            "          file_1.py  <== selected  <== marked",
3459            "          file_2.py",
3460            "          file_3.py",
3461            "    > dir_2",
3462            "      .gitignore",
3463        ],
3464        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3465    );
3466
3467    panel.update(cx, |panel, cx| {
3468        panel.project.update(cx, |_, cx| {
3469            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3470        })
3471    });
3472    cx.run_until_parked();
3473    assert_eq!(
3474        visible_entries_as_strings(&panel, 0..20, cx),
3475        &[
3476            "v project_root",
3477            "    > .git",
3478            "    v dir_1",
3479            "        > gitignored_dir",
3480            "          file_1.py",
3481            "          file_2.py",
3482            "          file_3.py",
3483            "    v dir_2",
3484            "          file_1.py  <== selected  <== marked",
3485            "          file_2.py",
3486            "          file_3.py",
3487            "      .gitignore",
3488        ],
3489        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3490    );
3491
3492    panel.update(cx, |panel, cx| {
3493        panel.project.update(cx, |_, cx| {
3494            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3495        })
3496    });
3497    cx.run_until_parked();
3498    assert_eq!(
3499        visible_entries_as_strings(&panel, 0..20, cx),
3500        &[
3501            "v project_root",
3502            "    > .git",
3503            "    v dir_1",
3504            "        v gitignored_dir",
3505            "              file_a.py  <== selected  <== marked",
3506            "              file_b.py",
3507            "              file_c.py",
3508            "          file_1.py",
3509            "          file_2.py",
3510            "          file_3.py",
3511            "    v dir_2",
3512            "          file_1.py",
3513            "          file_2.py",
3514            "          file_3.py",
3515            "      .gitignore",
3516        ],
3517        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3518    );
3519}
3520
3521#[gpui::test]
3522async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3523    init_test(cx);
3524    cx.update(|cx| {
3525        cx.update_global::<SettingsStore, _>(|store, cx| {
3526            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3527                project_settings.file_scan_exclusions =
3528                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3529            });
3530        });
3531    });
3532
3533    cx.update(|cx| {
3534        register_project_item::<TestProjectItemView>(cx);
3535    });
3536
3537    let fs = FakeFs::new(cx.executor().clone());
3538    fs.insert_tree(
3539        "/root1",
3540        json!({
3541            ".dockerignore": "",
3542            ".git": {
3543                "HEAD": "",
3544            },
3545        }),
3546    )
3547    .await;
3548
3549    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3550    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3551    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3552    let panel = workspace
3553        .update(cx, |workspace, window, cx| {
3554            let panel = ProjectPanel::new(workspace, window, cx);
3555            workspace.add_panel(panel.clone(), window, cx);
3556            panel
3557        })
3558        .unwrap();
3559
3560    select_path(&panel, "root1", cx);
3561    assert_eq!(
3562        visible_entries_as_strings(&panel, 0..10, cx),
3563        &["v root1  <== selected", "      .dockerignore",]
3564    );
3565    workspace
3566        .update(cx, |workspace, _, cx| {
3567            assert!(
3568                workspace.active_item(cx).is_none(),
3569                "Should have no active items in the beginning"
3570            );
3571        })
3572        .unwrap();
3573
3574    let excluded_file_path = ".git/COMMIT_EDITMSG";
3575    let excluded_dir_path = "excluded_dir";
3576
3577    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3578    panel.update_in(cx, |panel, window, cx| {
3579        assert!(panel.filename_editor.read(cx).is_focused(window));
3580    });
3581    panel
3582        .update_in(cx, |panel, window, cx| {
3583            panel.filename_editor.update(cx, |editor, cx| {
3584                editor.set_text(excluded_file_path, window, cx)
3585            });
3586            panel.confirm_edit(window, cx).unwrap()
3587        })
3588        .await
3589        .unwrap();
3590
3591    assert_eq!(
3592        visible_entries_as_strings(&panel, 0..13, cx),
3593        &["v root1", "      .dockerignore"],
3594        "Excluded dir should not be shown after opening a file in it"
3595    );
3596    panel.update_in(cx, |panel, window, cx| {
3597        assert!(
3598            !panel.filename_editor.read(cx).is_focused(window),
3599            "Should have closed the file name editor"
3600        );
3601    });
3602    workspace
3603        .update(cx, |workspace, _, cx| {
3604            let active_entry_path = workspace
3605                .active_item(cx)
3606                .expect("should have opened and activated the excluded item")
3607                .act_as::<TestProjectItemView>(cx)
3608                .expect("should have opened the corresponding project item for the excluded item")
3609                .read(cx)
3610                .path
3611                .clone();
3612            assert_eq!(
3613                active_entry_path.path.as_ref(),
3614                Path::new(excluded_file_path),
3615                "Should open the excluded file"
3616            );
3617
3618            assert!(
3619                workspace.notification_ids().is_empty(),
3620                "Should have no notifications after opening an excluded file"
3621            );
3622        })
3623        .unwrap();
3624    assert!(
3625        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3626        "Should have created the excluded file"
3627    );
3628
3629    select_path(&panel, "root1", cx);
3630    panel.update_in(cx, |panel, window, cx| {
3631        panel.new_directory(&NewDirectory, window, cx)
3632    });
3633    panel.update_in(cx, |panel, window, cx| {
3634        assert!(panel.filename_editor.read(cx).is_focused(window));
3635    });
3636    panel
3637        .update_in(cx, |panel, window, cx| {
3638            panel.filename_editor.update(cx, |editor, cx| {
3639                editor.set_text(excluded_file_path, window, cx)
3640            });
3641            panel.confirm_edit(window, cx).unwrap()
3642        })
3643        .await
3644        .unwrap();
3645
3646    assert_eq!(
3647        visible_entries_as_strings(&panel, 0..13, cx),
3648        &["v root1", "      .dockerignore"],
3649        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3650    );
3651    panel.update_in(cx, |panel, window, cx| {
3652        assert!(
3653            !panel.filename_editor.read(cx).is_focused(window),
3654            "Should have closed the file name editor"
3655        );
3656    });
3657    workspace
3658        .update(cx, |workspace, _, cx| {
3659            let notifications = workspace.notification_ids();
3660            assert_eq!(
3661                notifications.len(),
3662                1,
3663                "Should receive one notification with the error message"
3664            );
3665            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3666            assert!(workspace.notification_ids().is_empty());
3667        })
3668        .unwrap();
3669
3670    select_path(&panel, "root1", cx);
3671    panel.update_in(cx, |panel, window, cx| {
3672        panel.new_directory(&NewDirectory, window, cx)
3673    });
3674    panel.update_in(cx, |panel, window, cx| {
3675        assert!(panel.filename_editor.read(cx).is_focused(window));
3676    });
3677    panel
3678        .update_in(cx, |panel, window, cx| {
3679            panel.filename_editor.update(cx, |editor, cx| {
3680                editor.set_text(excluded_dir_path, window, cx)
3681            });
3682            panel.confirm_edit(window, cx).unwrap()
3683        })
3684        .await
3685        .unwrap();
3686
3687    assert_eq!(
3688        visible_entries_as_strings(&panel, 0..13, cx),
3689        &["v root1", "      .dockerignore"],
3690        "Should not change the project panel after trying to create an excluded directory"
3691    );
3692    panel.update_in(cx, |panel, window, cx| {
3693        assert!(
3694            !panel.filename_editor.read(cx).is_focused(window),
3695            "Should have closed the file name editor"
3696        );
3697    });
3698    workspace
3699        .update(cx, |workspace, _, cx| {
3700            let notifications = workspace.notification_ids();
3701            assert_eq!(
3702                notifications.len(),
3703                1,
3704                "Should receive one notification explaining that no directory is actually shown"
3705            );
3706            workspace.dismiss_notification(notifications.first().unwrap(), cx);
3707            assert!(workspace.notification_ids().is_empty());
3708        })
3709        .unwrap();
3710    assert!(
3711        fs.is_dir(Path::new("/root1/excluded_dir")).await,
3712        "Should have created the excluded directory"
3713    );
3714}
3715
3716#[gpui::test]
3717async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3718    init_test_with_editor(cx);
3719
3720    let fs = FakeFs::new(cx.executor().clone());
3721    fs.insert_tree(
3722        "/src",
3723        json!({
3724            "test": {
3725                "first.rs": "// First Rust file",
3726                "second.rs": "// Second Rust file",
3727                "third.rs": "// Third Rust file",
3728            }
3729        }),
3730    )
3731    .await;
3732
3733    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3734    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3735    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3736    let panel = workspace
3737        .update(cx, |workspace, window, cx| {
3738            let panel = ProjectPanel::new(workspace, window, cx);
3739            workspace.add_panel(panel.clone(), window, cx);
3740            panel
3741        })
3742        .unwrap();
3743
3744    select_path(&panel, "src/", cx);
3745    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3746    cx.executor().run_until_parked();
3747    assert_eq!(
3748        visible_entries_as_strings(&panel, 0..10, cx),
3749        &[
3750            //
3751            "v src  <== selected",
3752            "    > test"
3753        ]
3754    );
3755    panel.update_in(cx, |panel, window, cx| {
3756        panel.new_directory(&NewDirectory, window, cx)
3757    });
3758    panel.update_in(cx, |panel, window, cx| {
3759        assert!(panel.filename_editor.read(cx).is_focused(window));
3760    });
3761    assert_eq!(
3762        visible_entries_as_strings(&panel, 0..10, cx),
3763        &[
3764            //
3765            "v src",
3766            "    > [EDITOR: '']  <== selected",
3767            "    > test"
3768        ]
3769    );
3770
3771    panel.update_in(cx, |panel, window, cx| {
3772        panel.cancel(&menu::Cancel, window, cx)
3773    });
3774    assert_eq!(
3775        visible_entries_as_strings(&panel, 0..10, cx),
3776        &[
3777            //
3778            "v src  <== selected",
3779            "    > test"
3780        ]
3781    );
3782}
3783
3784#[gpui::test]
3785async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3786    init_test_with_editor(cx);
3787
3788    let fs = FakeFs::new(cx.executor().clone());
3789    fs.insert_tree(
3790        "/root",
3791        json!({
3792            "dir1": {
3793                "subdir1": {},
3794                "file1.txt": "",
3795                "file2.txt": "",
3796            },
3797            "dir2": {
3798                "subdir2": {},
3799                "file3.txt": "",
3800                "file4.txt": "",
3801            },
3802            "file5.txt": "",
3803            "file6.txt": "",
3804        }),
3805    )
3806    .await;
3807
3808    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3809    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3810    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3811    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3812
3813    toggle_expand_dir(&panel, "root/dir1", cx);
3814    toggle_expand_dir(&panel, "root/dir2", cx);
3815
3816    // Test Case 1: Delete middle file in directory
3817    select_path(&panel, "root/dir1/file1.txt", cx);
3818    assert_eq!(
3819        visible_entries_as_strings(&panel, 0..15, cx),
3820        &[
3821            "v root",
3822            "    v dir1",
3823            "        > subdir1",
3824            "          file1.txt  <== selected",
3825            "          file2.txt",
3826            "    v dir2",
3827            "        > subdir2",
3828            "          file3.txt",
3829            "          file4.txt",
3830            "      file5.txt",
3831            "      file6.txt",
3832        ],
3833        "Initial state before deleting middle file"
3834    );
3835
3836    submit_deletion(&panel, cx);
3837    assert_eq!(
3838        visible_entries_as_strings(&panel, 0..15, cx),
3839        &[
3840            "v root",
3841            "    v dir1",
3842            "        > subdir1",
3843            "          file2.txt  <== selected",
3844            "    v dir2",
3845            "        > subdir2",
3846            "          file3.txt",
3847            "          file4.txt",
3848            "      file5.txt",
3849            "      file6.txt",
3850        ],
3851        "Should select next file after deleting middle file"
3852    );
3853
3854    // Test Case 2: Delete last file in directory
3855    submit_deletion(&panel, cx);
3856    assert_eq!(
3857        visible_entries_as_strings(&panel, 0..15, cx),
3858        &[
3859            "v root",
3860            "    v dir1",
3861            "        > subdir1  <== selected",
3862            "    v dir2",
3863            "        > subdir2",
3864            "          file3.txt",
3865            "          file4.txt",
3866            "      file5.txt",
3867            "      file6.txt",
3868        ],
3869        "Should select next directory when last file is deleted"
3870    );
3871
3872    // Test Case 3: Delete root level file
3873    select_path(&panel, "root/file6.txt", cx);
3874    assert_eq!(
3875        visible_entries_as_strings(&panel, 0..15, cx),
3876        &[
3877            "v root",
3878            "    v dir1",
3879            "        > subdir1",
3880            "    v dir2",
3881            "        > subdir2",
3882            "          file3.txt",
3883            "          file4.txt",
3884            "      file5.txt",
3885            "      file6.txt  <== selected",
3886        ],
3887        "Initial state before deleting root level file"
3888    );
3889
3890    submit_deletion(&panel, cx);
3891    assert_eq!(
3892        visible_entries_as_strings(&panel, 0..15, cx),
3893        &[
3894            "v root",
3895            "    v dir1",
3896            "        > subdir1",
3897            "    v dir2",
3898            "        > subdir2",
3899            "          file3.txt",
3900            "          file4.txt",
3901            "      file5.txt  <== selected",
3902        ],
3903        "Should select prev entry at root level"
3904    );
3905}
3906
3907#[gpui::test]
3908async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3909    init_test_with_editor(cx);
3910
3911    let fs = FakeFs::new(cx.executor().clone());
3912    fs.insert_tree(
3913        path!("/root"),
3914        json!({
3915            "aa": "// Testing 1",
3916            "bb": "// Testing 2",
3917            "cc": "// Testing 3",
3918            "dd": "// Testing 4",
3919            "ee": "// Testing 5",
3920            "ff": "// Testing 6",
3921            "gg": "// Testing 7",
3922            "hh": "// Testing 8",
3923            "ii": "// Testing 8",
3924            ".gitignore": "bb\ndd\nee\nff\nii\n'",
3925        }),
3926    )
3927    .await;
3928
3929    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3930    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3931    let cx = &mut VisualTestContext::from_window(*workspace, cx);
3932
3933    // Test 1: Auto selection with one gitignored file next to the deleted file
3934    cx.update(|_, cx| {
3935        let settings = *ProjectPanelSettings::get_global(cx);
3936        ProjectPanelSettings::override_global(
3937            ProjectPanelSettings {
3938                hide_gitignore: true,
3939                ..settings
3940            },
3941            cx,
3942        );
3943    });
3944
3945    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3946
3947    select_path(&panel, "root/aa", cx);
3948    assert_eq!(
3949        visible_entries_as_strings(&panel, 0..10, cx),
3950        &[
3951            "v root",
3952            "      .gitignore",
3953            "      aa  <== selected",
3954            "      cc",
3955            "      gg",
3956            "      hh"
3957        ],
3958        "Initial state should hide files on .gitignore"
3959    );
3960
3961    submit_deletion(&panel, cx);
3962
3963    assert_eq!(
3964        visible_entries_as_strings(&panel, 0..10, cx),
3965        &[
3966            "v root",
3967            "      .gitignore",
3968            "      cc  <== selected",
3969            "      gg",
3970            "      hh"
3971        ],
3972        "Should select next entry not on .gitignore"
3973    );
3974
3975    // Test 2: Auto selection with many gitignored files next to the deleted file
3976    submit_deletion(&panel, cx);
3977    assert_eq!(
3978        visible_entries_as_strings(&panel, 0..10, cx),
3979        &[
3980            "v root",
3981            "      .gitignore",
3982            "      gg  <== selected",
3983            "      hh"
3984        ],
3985        "Should select next entry not on .gitignore"
3986    );
3987
3988    // Test 3: Auto selection of entry before deleted file
3989    select_path(&panel, "root/hh", cx);
3990    assert_eq!(
3991        visible_entries_as_strings(&panel, 0..10, cx),
3992        &[
3993            "v root",
3994            "      .gitignore",
3995            "      gg",
3996            "      hh  <== selected"
3997        ],
3998        "Should select next entry not on .gitignore"
3999    );
4000    submit_deletion(&panel, cx);
4001    assert_eq!(
4002        visible_entries_as_strings(&panel, 0..10, cx),
4003        &["v root", "      .gitignore", "      gg  <== selected"],
4004        "Should select next entry not on .gitignore"
4005    );
4006}
4007
4008#[gpui::test]
4009async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
4010    init_test_with_editor(cx);
4011
4012    let fs = FakeFs::new(cx.executor().clone());
4013    fs.insert_tree(
4014        path!("/root"),
4015        json!({
4016            "dir1": {
4017                "file1": "// Testing",
4018                "file2": "// Testing",
4019                "file3": "// Testing"
4020            },
4021            "aa": "// Testing",
4022            ".gitignore": "file1\nfile3\n",
4023        }),
4024    )
4025    .await;
4026
4027    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4028    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4029    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4030
4031    cx.update(|_, cx| {
4032        let settings = *ProjectPanelSettings::get_global(cx);
4033        ProjectPanelSettings::override_global(
4034            ProjectPanelSettings {
4035                hide_gitignore: true,
4036                ..settings
4037            },
4038            cx,
4039        );
4040    });
4041
4042    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4043
4044    // Test 1: Visible items should exclude files on gitignore
4045    toggle_expand_dir(&panel, "root/dir1", cx);
4046    select_path(&panel, "root/dir1/file2", cx);
4047    assert_eq!(
4048        visible_entries_as_strings(&panel, 0..10, cx),
4049        &[
4050            "v root",
4051            "    v dir1",
4052            "          file2  <== selected",
4053            "      .gitignore",
4054            "      aa"
4055        ],
4056        "Initial state should hide files on .gitignore"
4057    );
4058    submit_deletion(&panel, cx);
4059
4060    // Test 2: Auto selection should go to the parent
4061    assert_eq!(
4062        visible_entries_as_strings(&panel, 0..10, cx),
4063        &[
4064            "v root",
4065            "    v dir1  <== selected",
4066            "      .gitignore",
4067            "      aa"
4068        ],
4069        "Initial state should hide files on .gitignore"
4070    );
4071}
4072
4073#[gpui::test]
4074async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
4075    init_test_with_editor(cx);
4076
4077    let fs = FakeFs::new(cx.executor().clone());
4078    fs.insert_tree(
4079        "/root",
4080        json!({
4081            "dir1": {
4082                "subdir1": {
4083                    "a.txt": "",
4084                    "b.txt": ""
4085                },
4086                "file1.txt": "",
4087            },
4088            "dir2": {
4089                "subdir2": {
4090                    "c.txt": "",
4091                    "d.txt": ""
4092                },
4093                "file2.txt": "",
4094            },
4095            "file3.txt": "",
4096        }),
4097    )
4098    .await;
4099
4100    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4101    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4102    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4103    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4104
4105    toggle_expand_dir(&panel, "root/dir1", cx);
4106    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4107    toggle_expand_dir(&panel, "root/dir2", cx);
4108    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4109
4110    // Test Case 1: Select and delete nested directory with parent
4111    cx.simulate_modifiers_change(gpui::Modifiers {
4112        control: true,
4113        ..Default::default()
4114    });
4115    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4116    select_path_with_mark(&panel, "root/dir1", cx);
4117
4118    assert_eq!(
4119        visible_entries_as_strings(&panel, 0..15, cx),
4120        &[
4121            "v root",
4122            "    v dir1  <== selected  <== marked",
4123            "        v subdir1  <== marked",
4124            "              a.txt",
4125            "              b.txt",
4126            "          file1.txt",
4127            "    v dir2",
4128            "        v subdir2",
4129            "              c.txt",
4130            "              d.txt",
4131            "          file2.txt",
4132            "      file3.txt",
4133        ],
4134        "Initial state before deleting nested directory with parent"
4135    );
4136
4137    submit_deletion(&panel, cx);
4138    assert_eq!(
4139        visible_entries_as_strings(&panel, 0..15, cx),
4140        &[
4141            "v root",
4142            "    v dir2  <== selected",
4143            "        v subdir2",
4144            "              c.txt",
4145            "              d.txt",
4146            "          file2.txt",
4147            "      file3.txt",
4148        ],
4149        "Should select next directory after deleting directory with parent"
4150    );
4151
4152    // Test Case 2: Select mixed files and directories across levels
4153    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4154    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4155    select_path_with_mark(&panel, "root/file3.txt", cx);
4156
4157    assert_eq!(
4158        visible_entries_as_strings(&panel, 0..15, cx),
4159        &[
4160            "v root",
4161            "    v dir2",
4162            "        v subdir2",
4163            "              c.txt  <== marked",
4164            "              d.txt",
4165            "          file2.txt  <== marked",
4166            "      file3.txt  <== selected  <== marked",
4167        ],
4168        "Initial state before deleting"
4169    );
4170
4171    submit_deletion(&panel, cx);
4172    assert_eq!(
4173        visible_entries_as_strings(&panel, 0..15, cx),
4174        &[
4175            "v root",
4176            "    v dir2  <== selected",
4177            "        v subdir2",
4178            "              d.txt",
4179        ],
4180        "Should select sibling directory"
4181    );
4182}
4183
4184#[gpui::test]
4185async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4186    init_test_with_editor(cx);
4187
4188    let fs = FakeFs::new(cx.executor().clone());
4189    fs.insert_tree(
4190        "/root",
4191        json!({
4192            "dir1": {
4193                "subdir1": {
4194                    "a.txt": "",
4195                    "b.txt": ""
4196                },
4197                "file1.txt": "",
4198            },
4199            "dir2": {
4200                "subdir2": {
4201                    "c.txt": "",
4202                    "d.txt": ""
4203                },
4204                "file2.txt": "",
4205            },
4206            "file3.txt": "",
4207            "file4.txt": "",
4208        }),
4209    )
4210    .await;
4211
4212    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4213    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4214    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4215    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4216
4217    toggle_expand_dir(&panel, "root/dir1", cx);
4218    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4219    toggle_expand_dir(&panel, "root/dir2", cx);
4220    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4221
4222    // Test Case 1: Select all root files and directories
4223    cx.simulate_modifiers_change(gpui::Modifiers {
4224        control: true,
4225        ..Default::default()
4226    });
4227    select_path_with_mark(&panel, "root/dir1", cx);
4228    select_path_with_mark(&panel, "root/dir2", cx);
4229    select_path_with_mark(&panel, "root/file3.txt", cx);
4230    select_path_with_mark(&panel, "root/file4.txt", cx);
4231    assert_eq!(
4232        visible_entries_as_strings(&panel, 0..20, cx),
4233        &[
4234            "v root",
4235            "    v dir1  <== marked",
4236            "        v subdir1",
4237            "              a.txt",
4238            "              b.txt",
4239            "          file1.txt",
4240            "    v dir2  <== marked",
4241            "        v subdir2",
4242            "              c.txt",
4243            "              d.txt",
4244            "          file2.txt",
4245            "      file3.txt  <== marked",
4246            "      file4.txt  <== selected  <== marked",
4247        ],
4248        "State before deleting all contents"
4249    );
4250
4251    submit_deletion(&panel, cx);
4252    assert_eq!(
4253        visible_entries_as_strings(&panel, 0..20, cx),
4254        &["v root  <== selected"],
4255        "Only empty root directory should remain after deleting all contents"
4256    );
4257}
4258
4259#[gpui::test]
4260async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4261    init_test_with_editor(cx);
4262
4263    let fs = FakeFs::new(cx.executor().clone());
4264    fs.insert_tree(
4265        "/root",
4266        json!({
4267            "dir1": {
4268                "subdir1": {
4269                    "file_a.txt": "content a",
4270                    "file_b.txt": "content b",
4271                },
4272                "subdir2": {
4273                    "file_c.txt": "content c",
4274                },
4275                "file1.txt": "content 1",
4276            },
4277            "dir2": {
4278                "file2.txt": "content 2",
4279            },
4280        }),
4281    )
4282    .await;
4283
4284    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4285    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4286    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4287    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4288
4289    toggle_expand_dir(&panel, "root/dir1", cx);
4290    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4291    toggle_expand_dir(&panel, "root/dir2", cx);
4292    cx.simulate_modifiers_change(gpui::Modifiers {
4293        control: true,
4294        ..Default::default()
4295    });
4296
4297    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4298    select_path_with_mark(&panel, "root/dir1", cx);
4299    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4300    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4301
4302    assert_eq!(
4303        visible_entries_as_strings(&panel, 0..20, cx),
4304        &[
4305            "v root",
4306            "    v dir1  <== marked",
4307            "        v subdir1  <== marked",
4308            "              file_a.txt  <== selected  <== marked",
4309            "              file_b.txt",
4310            "        > subdir2",
4311            "          file1.txt",
4312            "    v dir2",
4313            "          file2.txt",
4314        ],
4315        "State with parent dir, subdir, and file selected"
4316    );
4317    submit_deletion(&panel, cx);
4318    assert_eq!(
4319        visible_entries_as_strings(&panel, 0..20, cx),
4320        &["v root", "    v dir2  <== selected", "          file2.txt",],
4321        "Only dir2 should remain after deletion"
4322    );
4323}
4324
4325#[gpui::test]
4326async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4327    init_test_with_editor(cx);
4328
4329    let fs = FakeFs::new(cx.executor().clone());
4330    // First worktree
4331    fs.insert_tree(
4332        "/root1",
4333        json!({
4334            "dir1": {
4335                "file1.txt": "content 1",
4336                "file2.txt": "content 2",
4337            },
4338            "dir2": {
4339                "file3.txt": "content 3",
4340            },
4341        }),
4342    )
4343    .await;
4344
4345    // Second worktree
4346    fs.insert_tree(
4347        "/root2",
4348        json!({
4349            "dir3": {
4350                "file4.txt": "content 4",
4351                "file5.txt": "content 5",
4352            },
4353            "file6.txt": "content 6",
4354        }),
4355    )
4356    .await;
4357
4358    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4359    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4360    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4361    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4362
4363    // Expand all directories for testing
4364    toggle_expand_dir(&panel, "root1/dir1", cx);
4365    toggle_expand_dir(&panel, "root1/dir2", cx);
4366    toggle_expand_dir(&panel, "root2/dir3", cx);
4367
4368    // Test Case 1: Delete files across different worktrees
4369    cx.simulate_modifiers_change(gpui::Modifiers {
4370        control: true,
4371        ..Default::default()
4372    });
4373    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4374    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4375
4376    assert_eq!(
4377        visible_entries_as_strings(&panel, 0..20, cx),
4378        &[
4379            "v root1",
4380            "    v dir1",
4381            "          file1.txt  <== marked",
4382            "          file2.txt",
4383            "    v dir2",
4384            "          file3.txt",
4385            "v root2",
4386            "    v dir3",
4387            "          file4.txt  <== selected  <== marked",
4388            "          file5.txt",
4389            "      file6.txt",
4390        ],
4391        "Initial state with files selected from different worktrees"
4392    );
4393
4394    submit_deletion(&panel, cx);
4395    assert_eq!(
4396        visible_entries_as_strings(&panel, 0..20, cx),
4397        &[
4398            "v root1",
4399            "    v dir1",
4400            "          file2.txt",
4401            "    v dir2",
4402            "          file3.txt",
4403            "v root2",
4404            "    v dir3",
4405            "          file5.txt  <== selected",
4406            "      file6.txt",
4407        ],
4408        "Should select next file in the last worktree after deletion"
4409    );
4410
4411    // Test Case 2: Delete directories from different worktrees
4412    select_path_with_mark(&panel, "root1/dir1", cx);
4413    select_path_with_mark(&panel, "root2/dir3", cx);
4414
4415    assert_eq!(
4416        visible_entries_as_strings(&panel, 0..20, cx),
4417        &[
4418            "v root1",
4419            "    v dir1  <== marked",
4420            "          file2.txt",
4421            "    v dir2",
4422            "          file3.txt",
4423            "v root2",
4424            "    v dir3  <== selected  <== marked",
4425            "          file5.txt",
4426            "      file6.txt",
4427        ],
4428        "State with directories marked from different worktrees"
4429    );
4430
4431    submit_deletion(&panel, cx);
4432    assert_eq!(
4433        visible_entries_as_strings(&panel, 0..20, cx),
4434        &[
4435            "v root1",
4436            "    v dir2",
4437            "          file3.txt",
4438            "v root2",
4439            "      file6.txt  <== selected",
4440        ],
4441        "Should select remaining file in last worktree after directory deletion"
4442    );
4443
4444    // Test Case 4: Delete all remaining files except roots
4445    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4446    select_path_with_mark(&panel, "root2/file6.txt", cx);
4447
4448    assert_eq!(
4449        visible_entries_as_strings(&panel, 0..20, cx),
4450        &[
4451            "v root1",
4452            "    v dir2",
4453            "          file3.txt  <== marked",
4454            "v root2",
4455            "      file6.txt  <== selected  <== marked",
4456        ],
4457        "State with all remaining files marked"
4458    );
4459
4460    submit_deletion(&panel, cx);
4461    assert_eq!(
4462        visible_entries_as_strings(&panel, 0..20, cx),
4463        &["v root1", "    v dir2", "v root2  <== selected"],
4464        "Second parent root should be selected after deleting"
4465    );
4466}
4467
4468#[gpui::test]
4469async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4470    init_test_with_editor(cx);
4471
4472    let fs = FakeFs::new(cx.executor().clone());
4473    fs.insert_tree(
4474        "/root",
4475        json!({
4476            "dir1": {
4477                "file1.txt": "",
4478                "file2.txt": "",
4479                "file3.txt": "",
4480            },
4481            "dir2": {
4482                "file4.txt": "",
4483                "file5.txt": "",
4484            },
4485        }),
4486    )
4487    .await;
4488
4489    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4490    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4491    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4492    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4493
4494    toggle_expand_dir(&panel, "root/dir1", cx);
4495    toggle_expand_dir(&panel, "root/dir2", cx);
4496
4497    cx.simulate_modifiers_change(gpui::Modifiers {
4498        control: true,
4499        ..Default::default()
4500    });
4501
4502    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4503    select_path(&panel, "root/dir1/file1.txt", cx);
4504
4505    assert_eq!(
4506        visible_entries_as_strings(&panel, 0..15, cx),
4507        &[
4508            "v root",
4509            "    v dir1",
4510            "          file1.txt  <== selected",
4511            "          file2.txt  <== marked",
4512            "          file3.txt",
4513            "    v dir2",
4514            "          file4.txt",
4515            "          file5.txt",
4516        ],
4517        "Initial state with one marked entry and different selection"
4518    );
4519
4520    // Delete should operate on the selected entry (file1.txt)
4521    submit_deletion(&panel, cx);
4522    assert_eq!(
4523        visible_entries_as_strings(&panel, 0..15, cx),
4524        &[
4525            "v root",
4526            "    v dir1",
4527            "          file2.txt  <== selected  <== marked",
4528            "          file3.txt",
4529            "    v dir2",
4530            "          file4.txt",
4531            "          file5.txt",
4532        ],
4533        "Should delete selected file, not marked file"
4534    );
4535
4536    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4537    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4538    select_path(&panel, "root/dir2/file5.txt", cx);
4539
4540    assert_eq!(
4541        visible_entries_as_strings(&panel, 0..15, cx),
4542        &[
4543            "v root",
4544            "    v dir1",
4545            "          file2.txt  <== marked",
4546            "          file3.txt  <== marked",
4547            "    v dir2",
4548            "          file4.txt  <== marked",
4549            "          file5.txt  <== selected",
4550        ],
4551        "Initial state with multiple marked entries and different selection"
4552    );
4553
4554    // Delete should operate on all marked entries, ignoring the selection
4555    submit_deletion(&panel, cx);
4556    assert_eq!(
4557        visible_entries_as_strings(&panel, 0..15, cx),
4558        &[
4559            "v root",
4560            "    v dir1",
4561            "    v dir2",
4562            "          file5.txt  <== selected",
4563        ],
4564        "Should delete all marked files, leaving only the selected file"
4565    );
4566}
4567
4568#[gpui::test]
4569async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4570    init_test_with_editor(cx);
4571
4572    let fs = FakeFs::new(cx.executor().clone());
4573    fs.insert_tree(
4574        "/root_b",
4575        json!({
4576            "dir1": {
4577                "file1.txt": "content 1",
4578                "file2.txt": "content 2",
4579            },
4580        }),
4581    )
4582    .await;
4583
4584    fs.insert_tree(
4585        "/root_c",
4586        json!({
4587            "dir2": {},
4588        }),
4589    )
4590    .await;
4591
4592    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4593    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4594    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4595    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4596
4597    toggle_expand_dir(&panel, "root_b/dir1", cx);
4598    toggle_expand_dir(&panel, "root_c/dir2", cx);
4599
4600    cx.simulate_modifiers_change(gpui::Modifiers {
4601        control: true,
4602        ..Default::default()
4603    });
4604    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4605    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4606
4607    assert_eq!(
4608        visible_entries_as_strings(&panel, 0..20, cx),
4609        &[
4610            "v root_b",
4611            "    v dir1",
4612            "          file1.txt  <== marked",
4613            "          file2.txt  <== selected  <== marked",
4614            "v root_c",
4615            "    v dir2",
4616        ],
4617        "Initial state with files marked in root_b"
4618    );
4619
4620    submit_deletion(&panel, cx);
4621    assert_eq!(
4622        visible_entries_as_strings(&panel, 0..20, cx),
4623        &[
4624            "v root_b",
4625            "    v dir1  <== selected",
4626            "v root_c",
4627            "    v dir2",
4628        ],
4629        "After deletion in root_b as it's last deletion, selection should be in root_b"
4630    );
4631
4632    select_path_with_mark(&panel, "root_c/dir2", cx);
4633
4634    submit_deletion(&panel, cx);
4635    assert_eq!(
4636        visible_entries_as_strings(&panel, 0..20, cx),
4637        &["v root_b", "    v dir1", "v root_c  <== selected",],
4638        "After deleting from root_c, it should remain in root_c"
4639    );
4640}
4641
4642fn toggle_expand_dir(
4643    panel: &Entity<ProjectPanel>,
4644    path: impl AsRef<Path>,
4645    cx: &mut VisualTestContext,
4646) {
4647    let path = path.as_ref();
4648    panel.update_in(cx, |panel, window, cx| {
4649        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4650            let worktree = worktree.read(cx);
4651            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4652                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4653                panel.toggle_expanded(entry_id, window, cx);
4654                return;
4655            }
4656        }
4657        panic!("no worktree for path {:?}", path);
4658    });
4659}
4660
4661#[gpui::test]
4662async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4663    init_test_with_editor(cx);
4664
4665    let fs = FakeFs::new(cx.executor().clone());
4666    fs.insert_tree(
4667        path!("/root"),
4668        json!({
4669            ".gitignore": "**/ignored_dir\n**/ignored_nested",
4670            "dir1": {
4671                "empty1": {
4672                    "empty2": {
4673                        "empty3": {
4674                            "file.txt": ""
4675                        }
4676                    }
4677                },
4678                "subdir1": {
4679                    "file1.txt": "",
4680                    "file2.txt": "",
4681                    "ignored_nested": {
4682                        "ignored_file.txt": ""
4683                    }
4684                },
4685                "ignored_dir": {
4686                    "subdir": {
4687                        "deep_file.txt": ""
4688                    }
4689                }
4690            }
4691        }),
4692    )
4693    .await;
4694
4695    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4696    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4697    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4698
4699    // Test 1: When auto-fold is enabled
4700    cx.update(|_, cx| {
4701        let settings = *ProjectPanelSettings::get_global(cx);
4702        ProjectPanelSettings::override_global(
4703            ProjectPanelSettings {
4704                auto_fold_dirs: true,
4705                ..settings
4706            },
4707            cx,
4708        );
4709    });
4710
4711    let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4712
4713    assert_eq!(
4714        visible_entries_as_strings(&panel, 0..20, cx),
4715        &["v root", "    > dir1", "      .gitignore",],
4716        "Initial state should show collapsed root structure"
4717    );
4718
4719    toggle_expand_dir(&panel, "root/dir1", cx);
4720    assert_eq!(
4721        visible_entries_as_strings(&panel, 0..20, cx),
4722        &[
4723            separator!("v root"),
4724            separator!("    v dir1  <== selected"),
4725            separator!("        > empty1/empty2/empty3"),
4726            separator!("        > ignored_dir"),
4727            separator!("        > subdir1"),
4728            separator!("      .gitignore"),
4729        ],
4730        "Should show first level with auto-folded dirs and ignored dir visible"
4731    );
4732
4733    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4734    panel.update(cx, |panel, cx| {
4735        let project = panel.project.read(cx);
4736        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4737        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4738        panel.update_visible_entries(None, cx);
4739    });
4740    cx.run_until_parked();
4741
4742    assert_eq!(
4743        visible_entries_as_strings(&panel, 0..20, cx),
4744        &[
4745            separator!("v root"),
4746            separator!("    v dir1  <== selected"),
4747            separator!("        v empty1"),
4748            separator!("            v empty2"),
4749            separator!("                v empty3"),
4750            separator!("                      file.txt"),
4751            separator!("        > ignored_dir"),
4752            separator!("        v subdir1"),
4753            separator!("            > ignored_nested"),
4754            separator!("              file1.txt"),
4755            separator!("              file2.txt"),
4756            separator!("      .gitignore"),
4757        ],
4758        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4759    );
4760
4761    // Test 2: When auto-fold is disabled
4762    cx.update(|_, cx| {
4763        let settings = *ProjectPanelSettings::get_global(cx);
4764        ProjectPanelSettings::override_global(
4765            ProjectPanelSettings {
4766                auto_fold_dirs: false,
4767                ..settings
4768            },
4769            cx,
4770        );
4771    });
4772
4773    panel.update_in(cx, |panel, window, cx| {
4774        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4775    });
4776
4777    toggle_expand_dir(&panel, "root/dir1", cx);
4778    assert_eq!(
4779        visible_entries_as_strings(&panel, 0..20, cx),
4780        &[
4781            separator!("v root"),
4782            separator!("    v dir1  <== selected"),
4783            separator!("        > empty1"),
4784            separator!("        > ignored_dir"),
4785            separator!("        > subdir1"),
4786            separator!("      .gitignore"),
4787        ],
4788        "With auto-fold disabled: should show all directories separately"
4789    );
4790
4791    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4792    panel.update(cx, |panel, cx| {
4793        let project = panel.project.read(cx);
4794        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4795        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4796        panel.update_visible_entries(None, cx);
4797    });
4798    cx.run_until_parked();
4799
4800    assert_eq!(
4801        visible_entries_as_strings(&panel, 0..20, cx),
4802        &[
4803            separator!("v root"),
4804            separator!("    v dir1  <== selected"),
4805            separator!("        v empty1"),
4806            separator!("            v empty2"),
4807            separator!("                v empty3"),
4808            separator!("                      file.txt"),
4809            separator!("        > ignored_dir"),
4810            separator!("        v subdir1"),
4811            separator!("            > ignored_nested"),
4812            separator!("              file1.txt"),
4813            separator!("              file2.txt"),
4814            separator!("      .gitignore"),
4815        ],
4816        "After expand_all without auto-fold: should expand all dirs normally, \
4817         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4818    );
4819
4820    // Test 3: When explicitly called on ignored directory
4821    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4822    panel.update(cx, |panel, cx| {
4823        let project = panel.project.read(cx);
4824        let worktree = project.worktrees(cx).next().unwrap().read(cx);
4825        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4826        panel.update_visible_entries(None, cx);
4827    });
4828    cx.run_until_parked();
4829
4830    assert_eq!(
4831        visible_entries_as_strings(&panel, 0..20, cx),
4832        &[
4833            separator!("v root"),
4834            separator!("    v dir1  <== selected"),
4835            separator!("        v empty1"),
4836            separator!("            v empty2"),
4837            separator!("                v empty3"),
4838            separator!("                      file.txt"),
4839            separator!("        v ignored_dir"),
4840            separator!("            v subdir"),
4841            separator!("                  deep_file.txt"),
4842            separator!("        v subdir1"),
4843            separator!("            > ignored_nested"),
4844            separator!("              file1.txt"),
4845            separator!("              file2.txt"),
4846            separator!("      .gitignore"),
4847        ],
4848        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4849    );
4850}
4851
4852#[gpui::test]
4853async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4854    init_test(cx);
4855
4856    let fs = FakeFs::new(cx.executor().clone());
4857    fs.insert_tree(
4858        path!("/root"),
4859        json!({
4860            "dir1": {
4861                "subdir1": {
4862                    "nested1": {
4863                        "file1.txt": "",
4864                        "file2.txt": ""
4865                    },
4866                },
4867                "subdir2": {
4868                    "file4.txt": ""
4869                }
4870            },
4871            "dir2": {
4872                "single_file": {
4873                    "file5.txt": ""
4874                }
4875            }
4876        }),
4877    )
4878    .await;
4879
4880    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4881    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4882    let cx = &mut VisualTestContext::from_window(*workspace, cx);
4883
4884    // Test 1: Basic collapsing
4885    {
4886        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4887
4888        toggle_expand_dir(&panel, "root/dir1", cx);
4889        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4890        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4891        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4892
4893        assert_eq!(
4894            visible_entries_as_strings(&panel, 0..20, cx),
4895            &[
4896                separator!("v root"),
4897                separator!("    v dir1"),
4898                separator!("        v subdir1"),
4899                separator!("            v nested1"),
4900                separator!("                  file1.txt"),
4901                separator!("                  file2.txt"),
4902                separator!("        v subdir2  <== selected"),
4903                separator!("              file4.txt"),
4904                separator!("    > dir2"),
4905            ],
4906            "Initial state with everything expanded"
4907        );
4908
4909        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4910        panel.update(cx, |panel, cx| {
4911            let project = panel.project.read(cx);
4912            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4913            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4914            panel.update_visible_entries(None, cx);
4915        });
4916
4917        assert_eq!(
4918            visible_entries_as_strings(&panel, 0..20, cx),
4919            &["v root", "    > dir1", "    > dir2",],
4920            "All subdirs under dir1 should be collapsed"
4921        );
4922    }
4923
4924    // Test 2: With auto-fold enabled
4925    {
4926        cx.update(|_, cx| {
4927            let settings = *ProjectPanelSettings::get_global(cx);
4928            ProjectPanelSettings::override_global(
4929                ProjectPanelSettings {
4930                    auto_fold_dirs: true,
4931                    ..settings
4932                },
4933                cx,
4934            );
4935        });
4936
4937        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4938
4939        toggle_expand_dir(&panel, "root/dir1", cx);
4940        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4941        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4942
4943        assert_eq!(
4944            visible_entries_as_strings(&panel, 0..20, cx),
4945            &[
4946                separator!("v root"),
4947                separator!("    v dir1"),
4948                separator!("        v subdir1/nested1  <== selected"),
4949                separator!("              file1.txt"),
4950                separator!("              file2.txt"),
4951                separator!("        > subdir2"),
4952                separator!("    > dir2/single_file"),
4953            ],
4954            "Initial state with some dirs expanded"
4955        );
4956
4957        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4958        panel.update(cx, |panel, cx| {
4959            let project = panel.project.read(cx);
4960            let worktree = project.worktrees(cx).next().unwrap().read(cx);
4961            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4962        });
4963
4964        toggle_expand_dir(&panel, "root/dir1", cx);
4965
4966        assert_eq!(
4967            visible_entries_as_strings(&panel, 0..20, cx),
4968            &[
4969                separator!("v root"),
4970                separator!("    v dir1  <== selected"),
4971                separator!("        > subdir1/nested1"),
4972                separator!("        > subdir2"),
4973                separator!("    > dir2/single_file"),
4974            ],
4975            "Subdirs should be collapsed and folded with auto-fold enabled"
4976        );
4977    }
4978
4979    // Test 3: With auto-fold disabled
4980    {
4981        cx.update(|_, cx| {
4982            let settings = *ProjectPanelSettings::get_global(cx);
4983            ProjectPanelSettings::override_global(
4984                ProjectPanelSettings {
4985                    auto_fold_dirs: false,
4986                    ..settings
4987                },
4988                cx,
4989            );
4990        });
4991
4992        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4993
4994        toggle_expand_dir(&panel, "root/dir1", cx);
4995        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4996        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4997
4998        assert_eq!(
4999            visible_entries_as_strings(&panel, 0..20, cx),
5000            &[
5001                separator!("v root"),
5002                separator!("    v dir1"),
5003                separator!("        v subdir1"),
5004                separator!("            v nested1  <== selected"),
5005                separator!("                  file1.txt"),
5006                separator!("                  file2.txt"),
5007                separator!("        > subdir2"),
5008                separator!("    > dir2"),
5009            ],
5010            "Initial state with some dirs expanded and auto-fold disabled"
5011        );
5012
5013        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5014        panel.update(cx, |panel, cx| {
5015            let project = panel.project.read(cx);
5016            let worktree = project.worktrees(cx).next().unwrap().read(cx);
5017            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5018        });
5019
5020        toggle_expand_dir(&panel, "root/dir1", cx);
5021
5022        assert_eq!(
5023            visible_entries_as_strings(&panel, 0..20, cx),
5024            &[
5025                separator!("v root"),
5026                separator!("    v dir1  <== selected"),
5027                separator!("        > subdir1"),
5028                separator!("        > subdir2"),
5029                separator!("    > dir2"),
5030            ],
5031            "Subdirs should be collapsed but not folded with auto-fold disabled"
5032        );
5033    }
5034}
5035
5036#[gpui::test]
5037async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
5038    init_test(cx);
5039
5040    let fs = FakeFs::new(cx.executor().clone());
5041    fs.insert_tree(
5042        path!("/root"),
5043        json!({
5044            "dir1": {
5045                "file1.txt": "",
5046            },
5047        }),
5048    )
5049    .await;
5050
5051    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5052    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5053    let cx = &mut VisualTestContext::from_window(*workspace, cx);
5054
5055    let panel = workspace
5056        .update(cx, |workspace, window, cx| {
5057            let panel = ProjectPanel::new(workspace, window, cx);
5058            workspace.add_panel(panel.clone(), window, cx);
5059            panel
5060        })
5061        .unwrap();
5062
5063    #[rustfmt::skip]
5064    assert_eq!(
5065        visible_entries_as_strings(&panel, 0..20, cx),
5066        &[
5067            separator!("v root"),
5068            separator!("    > dir1"),
5069        ],
5070        "Initial state with nothing selected"
5071    );
5072
5073    panel.update_in(cx, |panel, window, cx| {
5074        panel.new_file(&NewFile, window, cx);
5075    });
5076    panel.update_in(cx, |panel, window, cx| {
5077        assert!(panel.filename_editor.read(cx).is_focused(window));
5078    });
5079    panel
5080        .update_in(cx, |panel, window, cx| {
5081            panel.filename_editor.update(cx, |editor, cx| {
5082                editor.set_text("hello_from_no_selections", window, cx)
5083            });
5084            panel.confirm_edit(window, cx).unwrap()
5085        })
5086        .await
5087        .unwrap();
5088
5089    #[rustfmt::skip]
5090    assert_eq!(
5091        visible_entries_as_strings(&panel, 0..20, cx),
5092        &[
5093            separator!("v root"),
5094            separator!("    > dir1"),
5095            separator!("      hello_from_no_selections  <== selected  <== marked"),
5096        ],
5097        "A new file is created under the root directory"
5098    );
5099}
5100
5101fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
5102    let path = path.as_ref();
5103    panel.update(cx, |panel, cx| {
5104        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5105            let worktree = worktree.read(cx);
5106            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5107                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5108                panel.selection = Some(crate::SelectedEntry {
5109                    worktree_id: worktree.id(),
5110                    entry_id,
5111                });
5112                return;
5113            }
5114        }
5115        panic!("no worktree for path {:?}", path);
5116    });
5117}
5118
5119fn select_path_with_mark(
5120    panel: &Entity<ProjectPanel>,
5121    path: impl AsRef<Path>,
5122    cx: &mut VisualTestContext,
5123) {
5124    let path = path.as_ref();
5125    panel.update(cx, |panel, cx| {
5126        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5127            let worktree = worktree.read(cx);
5128            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5129                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5130                let entry = crate::SelectedEntry {
5131                    worktree_id: worktree.id(),
5132                    entry_id,
5133                };
5134                if !panel.marked_entries.contains(&entry) {
5135                    panel.marked_entries.insert(entry);
5136                }
5137                panel.selection = Some(entry);
5138                return;
5139            }
5140        }
5141        panic!("no worktree for path {:?}", path);
5142    });
5143}
5144
5145fn find_project_entry(
5146    panel: &Entity<ProjectPanel>,
5147    path: impl AsRef<Path>,
5148    cx: &mut VisualTestContext,
5149) -> Option<ProjectEntryId> {
5150    let path = path.as_ref();
5151    panel.update(cx, |panel, cx| {
5152        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5153            let worktree = worktree.read(cx);
5154            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5155                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
5156            }
5157        }
5158        panic!("no worktree for path {path:?}");
5159    })
5160}
5161
5162fn visible_entries_as_strings(
5163    panel: &Entity<ProjectPanel>,
5164    range: Range<usize>,
5165    cx: &mut VisualTestContext,
5166) -> Vec<String> {
5167    let mut result = Vec::new();
5168    let mut project_entries = HashSet::default();
5169    let mut has_editor = false;
5170
5171    panel.update_in(cx, |panel, window, cx| {
5172        panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
5173            if details.is_editing {
5174                assert!(!has_editor, "duplicate editor entry");
5175                has_editor = true;
5176            } else {
5177                assert!(
5178                    project_entries.insert(project_entry),
5179                    "duplicate project entry {:?} {:?}",
5180                    project_entry,
5181                    details
5182                );
5183            }
5184
5185            let indent = "    ".repeat(details.depth);
5186            let icon = if details.kind.is_dir() {
5187                if details.is_expanded { "v " } else { "> " }
5188            } else {
5189                "  "
5190            };
5191            let name = if details.is_editing {
5192                format!("[EDITOR: '{}']", details.filename)
5193            } else if details.is_processing {
5194                format!("[PROCESSING: '{}']", details.filename)
5195            } else {
5196                details.filename.clone()
5197            };
5198            let selected = if details.is_selected {
5199                "  <== selected"
5200            } else {
5201                ""
5202            };
5203            let marked = if details.is_marked {
5204                "  <== marked"
5205            } else {
5206                ""
5207            };
5208
5209            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5210        });
5211    });
5212
5213    result
5214}
5215
5216fn init_test(cx: &mut TestAppContext) {
5217    cx.update(|cx| {
5218        let settings_store = SettingsStore::test(cx);
5219        cx.set_global(settings_store);
5220        init_settings(cx);
5221        theme::init(theme::LoadThemes::JustBase, cx);
5222        language::init(cx);
5223        editor::init_settings(cx);
5224        crate::init(cx);
5225        workspace::init_settings(cx);
5226        client::init_settings(cx);
5227        Project::init_settings(cx);
5228
5229        cx.update_global::<SettingsStore, _>(|store, cx| {
5230            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5231                project_panel_settings.auto_fold_dirs = Some(false);
5232            });
5233            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5234                worktree_settings.file_scan_exclusions = Some(Vec::new());
5235            });
5236        });
5237    });
5238}
5239
5240fn init_test_with_editor(cx: &mut TestAppContext) {
5241    cx.update(|cx| {
5242        let app_state = AppState::test(cx);
5243        theme::init(theme::LoadThemes::JustBase, cx);
5244        init_settings(cx);
5245        language::init(cx);
5246        editor::init(cx);
5247        crate::init(cx);
5248        workspace::init(app_state.clone(), cx);
5249        Project::init_settings(cx);
5250
5251        cx.update_global::<SettingsStore, _>(|store, cx| {
5252            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5253                project_panel_settings.auto_fold_dirs = Some(false);
5254            });
5255            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5256                worktree_settings.file_scan_exclusions = Some(Vec::new());
5257            });
5258        });
5259    });
5260}
5261
5262fn ensure_single_file_is_opened(
5263    window: &WindowHandle<Workspace>,
5264    expected_path: &str,
5265    cx: &mut TestAppContext,
5266) {
5267    window
5268        .update(cx, |workspace, _, cx| {
5269            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5270            assert_eq!(worktrees.len(), 1);
5271            let worktree_id = worktrees[0].read(cx).id();
5272
5273            let open_project_paths = workspace
5274                .panes()
5275                .iter()
5276                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5277                .collect::<Vec<_>>();
5278            assert_eq!(
5279                open_project_paths,
5280                vec![ProjectPath {
5281                    worktree_id,
5282                    path: Arc::from(Path::new(expected_path))
5283                }],
5284                "Should have opened file, selected in project panel"
5285            );
5286        })
5287        .unwrap();
5288}
5289
5290fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5291    assert!(
5292        !cx.has_pending_prompt(),
5293        "Should have no prompts before the deletion"
5294    );
5295    panel.update_in(cx, |panel, window, cx| {
5296        panel.delete(&Delete { skip_prompt: false }, window, cx)
5297    });
5298    assert!(
5299        cx.has_pending_prompt(),
5300        "Should have a prompt after the deletion"
5301    );
5302    cx.simulate_prompt_answer("Delete");
5303    assert!(
5304        !cx.has_pending_prompt(),
5305        "Should have no prompts after prompt was replied to"
5306    );
5307    cx.executor().run_until_parked();
5308}
5309
5310fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5311    assert!(
5312        !cx.has_pending_prompt(),
5313        "Should have no prompts before the deletion"
5314    );
5315    panel.update_in(cx, |panel, window, cx| {
5316        panel.delete(&Delete { skip_prompt: true }, window, cx)
5317    });
5318    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5319    cx.executor().run_until_parked();
5320}
5321
5322fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5323    assert!(
5324        !cx.has_pending_prompt(),
5325        "Should have no prompts after deletion operation closes the file"
5326    );
5327    workspace
5328        .read_with(cx, |workspace, cx| {
5329            let open_project_paths = workspace
5330                .panes()
5331                .iter()
5332                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5333                .collect::<Vec<_>>();
5334            assert!(
5335                open_project_paths.is_empty(),
5336                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5337            );
5338        })
5339        .unwrap();
5340}
5341
5342struct TestProjectItemView {
5343    focus_handle: FocusHandle,
5344    path: ProjectPath,
5345}
5346
5347struct TestProjectItem {
5348    path: ProjectPath,
5349}
5350
5351impl project::ProjectItem for TestProjectItem {
5352    fn try_open(
5353        _project: &Entity<Project>,
5354        path: &ProjectPath,
5355        cx: &mut App,
5356    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
5357        let path = path.clone();
5358        Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5359    }
5360
5361    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5362        None
5363    }
5364
5365    fn project_path(&self, _: &App) -> Option<ProjectPath> {
5366        Some(self.path.clone())
5367    }
5368
5369    fn is_dirty(&self) -> bool {
5370        false
5371    }
5372}
5373
5374impl ProjectItem for TestProjectItemView {
5375    type Item = TestProjectItem;
5376
5377    fn for_project_item(
5378        _: Entity<Project>,
5379        _: Option<&Pane>,
5380        project_item: Entity<Self::Item>,
5381        _: &mut Window,
5382        cx: &mut Context<Self>,
5383    ) -> Self
5384    where
5385        Self: Sized,
5386    {
5387        Self {
5388            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5389            focus_handle: cx.focus_handle(),
5390        }
5391    }
5392}
5393
5394impl Item for TestProjectItemView {
5395    type Event = ();
5396
5397    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
5398        "Test".into()
5399    }
5400}
5401
5402impl EventEmitter<()> for TestProjectItemView {}
5403
5404impl Focusable for TestProjectItemView {
5405    fn focus_handle(&self, _: &App) -> FocusHandle {
5406        self.focus_handle.clone()
5407    }
5408}
5409
5410impl Render for TestProjectItemView {
5411    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5412        Empty
5413    }
5414}