project_panel_tests.rs

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