project_panel_tests.rs

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