project_panel_tests.rs

    1use super::*;
    2use collections::HashSet;
    3use editor::MultiBufferOffset;
    4use gpui::{Empty, Entity, TestAppContext, VisualTestContext};
    5use menu::Cancel;
    6use pretty_assertions::assert_eq;
    7use project::{FakeFs, ProjectPath};
    8use serde_json::json;
    9use settings::{ProjectPanelAutoOpenSettings, SettingsStore};
   10use std::path::{Path, PathBuf};
   11use util::{path, paths::PathStyle, rel_path::rel_path};
   12use workspace::{
   13    AppState, ItemHandle, MultiWorkspace, Pane, Workspace,
   14    item::{Item, ProjectItem},
   15    register_project_item,
   16};
   17
   18#[gpui::test]
   19async fn test_visible_list(cx: &mut gpui::TestAppContext) {
   20    init_test(cx);
   21
   22    let fs = FakeFs::new(cx.executor());
   23    fs.insert_tree(
   24        "/root1",
   25        json!({
   26            ".dockerignore": "",
   27            ".git": {
   28                "HEAD": "",
   29            },
   30            "a": {
   31                "0": { "q": "", "r": "", "s": "" },
   32                "1": { "t": "", "u": "" },
   33                "2": { "v": "", "w": "", "x": "", "y": "" },
   34            },
   35            "b": {
   36                "3": { "Q": "" },
   37                "4": { "R": "", "S": "", "T": "", "U": "" },
   38            },
   39            "C": {
   40                "5": {},
   41                "6": { "V": "", "W": "" },
   42                "7": { "X": "" },
   43                "8": { "Y": {}, "Z": "" }
   44            }
   45        }),
   46    )
   47    .await;
   48    fs.insert_tree(
   49        "/root2",
   50        json!({
   51            "d": {
   52                "9": ""
   53            },
   54            "e": {}
   55        }),
   56    )
   57    .await;
   58
   59    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
   60    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
   61    let workspace = window
   62        .read_with(cx, |mw, _| mw.workspace().clone())
   63        .unwrap();
   64    let cx = &mut VisualTestContext::from_window(window.into(), cx);
   65    let panel = workspace.update_in(cx, ProjectPanel::new);
   66    cx.run_until_parked();
   67    assert_eq!(
   68        visible_entries_as_strings(&panel, 0..50, cx),
   69        &[
   70            "v root1",
   71            "    > .git",
   72            "    > a",
   73            "    > b",
   74            "    > C",
   75            "      .dockerignore",
   76            "v root2",
   77            "    > d",
   78            "    > e",
   79        ]
   80    );
   81
   82    toggle_expand_dir(&panel, "root1/b", cx);
   83    assert_eq!(
   84        visible_entries_as_strings(&panel, 0..50, cx),
   85        &[
   86            "v root1",
   87            "    > .git",
   88            "    > a",
   89            "    v b  <== selected",
   90            "        > 3",
   91            "        > 4",
   92            "    > C",
   93            "      .dockerignore",
   94            "v root2",
   95            "    > d",
   96            "    > e",
   97        ]
   98    );
   99
  100    assert_eq!(
  101        visible_entries_as_strings(&panel, 6..9, cx),
  102        &[
  103            //
  104            "    > C",
  105            "      .dockerignore",
  106            "v root2",
  107        ]
  108    );
  109}
  110
  111#[gpui::test]
  112async fn test_opening_file(cx: &mut gpui::TestAppContext) {
  113    init_test_with_editor(cx);
  114
  115    let fs = FakeFs::new(cx.executor());
  116    fs.insert_tree(
  117        path!("/src"),
  118        json!({
  119            "test": {
  120                "first.rs": "// First Rust file",
  121                "second.rs": "// Second Rust file",
  122                "third.rs": "// Third Rust file",
  123            }
  124        }),
  125    )
  126    .await;
  127
  128    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
  129    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  130    let workspace = window
  131        .read_with(cx, |mw, _| mw.workspace().clone())
  132        .unwrap();
  133    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  134    let panel = workspace.update_in(cx, ProjectPanel::new);
  135    cx.run_until_parked();
  136
  137    toggle_expand_dir(&panel, "src/test", cx);
  138    select_path(&panel, "src/test/first.rs", cx);
  139    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
  140    cx.executor().run_until_parked();
  141    assert_eq!(
  142        visible_entries_as_strings(&panel, 0..10, cx),
  143        &[
  144            "v src",
  145            "    v test",
  146            "          first.rs  <== selected  <== marked",
  147            "          second.rs",
  148            "          third.rs"
  149        ]
  150    );
  151    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
  152
  153    select_path(&panel, "src/test/second.rs", cx);
  154    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
  155    cx.executor().run_until_parked();
  156    assert_eq!(
  157        visible_entries_as_strings(&panel, 0..10, cx),
  158        &[
  159            "v src",
  160            "    v test",
  161            "          first.rs",
  162            "          second.rs  <== selected  <== marked",
  163            "          third.rs"
  164        ]
  165    );
  166    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
  167}
  168
  169#[gpui::test]
  170async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
  171    init_test(cx);
  172    cx.update(|cx| {
  173        cx.update_global::<SettingsStore, _>(|store, cx| {
  174            store.update_user_settings(cx, |settings| {
  175                settings.project.worktree.file_scan_exclusions =
  176                    Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
  177            });
  178        });
  179    });
  180
  181    let fs = FakeFs::new(cx.background_executor.clone());
  182    fs.insert_tree(
  183        "/root1",
  184        json!({
  185            ".dockerignore": "",
  186            ".git": {
  187                "HEAD": "",
  188            },
  189            "a": {
  190                "0": { "q": "", "r": "", "s": "" },
  191                "1": { "t": "", "u": "" },
  192                "2": { "v": "", "w": "", "x": "", "y": "" },
  193            },
  194            "b": {
  195                "3": { "Q": "" },
  196                "4": { "R": "", "S": "", "T": "", "U": "" },
  197            },
  198            "C": {
  199                "5": {},
  200                "6": { "V": "", "W": "" },
  201                "7": { "X": "" },
  202                "8": { "Y": {}, "Z": "" }
  203            }
  204        }),
  205    )
  206    .await;
  207    fs.insert_tree(
  208        "/root2",
  209        json!({
  210            "d": {
  211                "4": ""
  212            },
  213            "e": {}
  214        }),
  215    )
  216    .await;
  217
  218    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  219    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  220    let workspace = window
  221        .read_with(cx, |mw, _| mw.workspace().clone())
  222        .unwrap();
  223    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  224    let panel = workspace.update_in(cx, ProjectPanel::new);
  225    cx.run_until_parked();
  226    assert_eq!(
  227        visible_entries_as_strings(&panel, 0..50, cx),
  228        &[
  229            "v root1",
  230            "    > a",
  231            "    > b",
  232            "    > C",
  233            "      .dockerignore",
  234            "v root2",
  235            "    > d",
  236            "    > e",
  237        ]
  238    );
  239
  240    toggle_expand_dir(&panel, "root1/b", cx);
  241    assert_eq!(
  242        visible_entries_as_strings(&panel, 0..50, cx),
  243        &[
  244            "v root1",
  245            "    > a",
  246            "    v b  <== selected",
  247            "        > 3",
  248            "    > C",
  249            "      .dockerignore",
  250            "v root2",
  251            "    > d",
  252            "    > e",
  253        ]
  254    );
  255
  256    toggle_expand_dir(&panel, "root2/d", cx);
  257    assert_eq!(
  258        visible_entries_as_strings(&panel, 0..50, cx),
  259        &[
  260            "v root1",
  261            "    > a",
  262            "    v b",
  263            "        > 3",
  264            "    > C",
  265            "      .dockerignore",
  266            "v root2",
  267            "    v d  <== selected",
  268            "    > e",
  269        ]
  270    );
  271
  272    toggle_expand_dir(&panel, "root2/e", cx);
  273    assert_eq!(
  274        visible_entries_as_strings(&panel, 0..50, cx),
  275        &[
  276            "v root1",
  277            "    > a",
  278            "    v b",
  279            "        > 3",
  280            "    > C",
  281            "      .dockerignore",
  282            "v root2",
  283            "    v d",
  284            "    v e  <== selected",
  285        ]
  286    );
  287}
  288
  289#[gpui::test]
  290async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
  291    init_test(cx);
  292
  293    let fs = FakeFs::new(cx.executor());
  294    fs.insert_tree(
  295        path!("/root1"),
  296        json!({
  297            "dir_1": {
  298                "nested_dir_1": {
  299                    "nested_dir_2": {
  300                        "nested_dir_3": {
  301                            "file_a.java": "// File contents",
  302                            "file_b.java": "// File contents",
  303                            "file_c.java": "// File contents",
  304                            "nested_dir_4": {
  305                                "nested_dir_5": {
  306                                    "file_d.java": "// File contents",
  307                                }
  308                            }
  309                        }
  310                    }
  311                }
  312            }
  313        }),
  314    )
  315    .await;
  316    fs.insert_tree(
  317        path!("/root2"),
  318        json!({
  319            "dir_2": {
  320                "file_1.java": "// File contents",
  321            }
  322        }),
  323    )
  324    .await;
  325
  326    // Test 1: Multiple worktrees with auto_fold_dirs = true
  327    let project = Project::test(
  328        fs.clone(),
  329        [path!("/root1").as_ref(), path!("/root2").as_ref()],
  330        cx,
  331    )
  332    .await;
  333    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  334    let workspace = window
  335        .read_with(cx, |mw, _| mw.workspace().clone())
  336        .unwrap();
  337    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  338    cx.update(|_, cx| {
  339        let settings = *ProjectPanelSettings::get_global(cx);
  340        ProjectPanelSettings::override_global(
  341            ProjectPanelSettings {
  342                auto_fold_dirs: true,
  343                sort_mode: settings::ProjectPanelSortMode::DirectoriesFirst,
  344                ..settings
  345            },
  346            cx,
  347        );
  348    });
  349    let panel = workspace.update_in(cx, ProjectPanel::new);
  350    cx.run_until_parked();
  351    assert_eq!(
  352        visible_entries_as_strings(&panel, 0..10, cx),
  353        &[
  354            "v root1",
  355            "    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  356            "v root2",
  357            "    > dir_2",
  358        ]
  359    );
  360
  361    toggle_expand_dir(
  362        &panel,
  363        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  364        cx,
  365    );
  366    assert_eq!(
  367        visible_entries_as_strings(&panel, 0..10, cx),
  368        &[
  369            "v root1",
  370            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
  371            "        > nested_dir_4/nested_dir_5",
  372            "          file_a.java",
  373            "          file_b.java",
  374            "          file_c.java",
  375            "v root2",
  376            "    > dir_2",
  377        ]
  378    );
  379
  380    toggle_expand_dir(
  381        &panel,
  382        "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
  383        cx,
  384    );
  385    assert_eq!(
  386        visible_entries_as_strings(&panel, 0..10, cx),
  387        &[
  388            "v root1",
  389            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  390            "        v nested_dir_4/nested_dir_5  <== selected",
  391            "              file_d.java",
  392            "          file_a.java",
  393            "          file_b.java",
  394            "          file_c.java",
  395            "v root2",
  396            "    > dir_2",
  397        ]
  398    );
  399    toggle_expand_dir(&panel, "root2/dir_2", cx);
  400    assert_eq!(
  401        visible_entries_as_strings(&panel, 0..10, cx),
  402        &[
  403            "v root1",
  404            "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  405            "        v nested_dir_4/nested_dir_5",
  406            "              file_d.java",
  407            "          file_a.java",
  408            "          file_b.java",
  409            "          file_c.java",
  410            "v root2",
  411            "    v dir_2  <== selected",
  412            "          file_1.java",
  413        ]
  414    );
  415
  416    // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
  417    {
  418        let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
  419        let window =
  420            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  421        let workspace = window
  422            .read_with(cx, |mw, _| mw.workspace().clone())
  423            .unwrap();
  424        let cx = &mut VisualTestContext::from_window(window.into(), cx);
  425        cx.update(|_, cx| {
  426            let settings = *ProjectPanelSettings::get_global(cx);
  427            ProjectPanelSettings::override_global(
  428                ProjectPanelSettings {
  429                    auto_fold_dirs: true,
  430                    hide_root: true,
  431                    ..settings
  432                },
  433                cx,
  434            );
  435        });
  436        let panel = workspace.update_in(cx, ProjectPanel::new);
  437        cx.run_until_parked();
  438        assert_eq!(
  439            visible_entries_as_strings(&panel, 0..10, cx),
  440            &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"],
  441            "Single worktree with hide_root=true should hide root and show auto-folded paths"
  442        );
  443
  444        toggle_expand_dir(
  445            &panel,
  446            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  447            cx,
  448        );
  449        assert_eq!(
  450            visible_entries_as_strings(&panel, 0..10, cx),
  451            &[
  452                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
  453                "    > nested_dir_4/nested_dir_5",
  454                "      file_a.java",
  455                "      file_b.java",
  456                "      file_c.java",
  457            ],
  458            "Expanded auto-folded path with hidden root should show contents without root prefix"
  459        );
  460
  461        toggle_expand_dir(
  462            &panel,
  463            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
  464            cx,
  465        );
  466        assert_eq!(
  467            visible_entries_as_strings(&panel, 0..10, cx),
  468            &[
  469                "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
  470                "    v nested_dir_4/nested_dir_5  <== selected",
  471                "          file_d.java",
  472                "      file_a.java",
  473                "      file_b.java",
  474                "      file_c.java",
  475            ],
  476            "Nested expansion with hidden root should maintain proper indentation"
  477        );
  478    }
  479}
  480
  481#[gpui::test(iterations = 30)]
  482async fn test_editing_files(cx: &mut gpui::TestAppContext) {
  483    init_test(cx);
  484
  485    let fs = FakeFs::new(cx.executor());
  486    fs.insert_tree(
  487        "/root1",
  488        json!({
  489            ".dockerignore": "",
  490            ".git": {
  491                "HEAD": "",
  492            },
  493            "a": {
  494                "0": { "q": "", "r": "", "s": "" },
  495                "1": { "t": "", "u": "" },
  496                "2": { "v": "", "w": "", "x": "", "y": "" },
  497            },
  498            "b": {
  499                "3": { "Q": "" },
  500                "4": { "R": "", "S": "", "T": "", "U": "" },
  501            },
  502            "C": {
  503                "5": {},
  504                "6": { "V": "", "W": "" },
  505                "7": { "X": "" },
  506                "8": { "Y": {}, "Z": "" }
  507            }
  508        }),
  509    )
  510    .await;
  511    fs.insert_tree(
  512        "/root2",
  513        json!({
  514            "d": {
  515                "9": ""
  516            },
  517            "e": {}
  518        }),
  519    )
  520    .await;
  521
  522    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  523    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  524    let workspace = window
  525        .read_with(cx, |mw, _| mw.workspace().clone())
  526        .unwrap();
  527    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  528    let panel = workspace.update_in(cx, |workspace, window, cx| {
  529        let panel = ProjectPanel::new(workspace, window, cx);
  530        workspace.add_panel(panel.clone(), window, cx);
  531        panel
  532    });
  533    cx.run_until_parked();
  534
  535    select_path(&panel, "root1", cx);
  536    assert_eq!(
  537        visible_entries_as_strings(&panel, 0..10, cx),
  538        &[
  539            "v root1  <== selected",
  540            "    > .git",
  541            "    > a",
  542            "    > b",
  543            "    > C",
  544            "      .dockerignore",
  545            "v root2",
  546            "    > d",
  547            "    > e",
  548        ]
  549    );
  550
  551    // Add a file with the root folder selected. The filename editor is placed
  552    // before the first file in the root folder.
  553    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
  554    cx.run_until_parked();
  555    panel.update_in(cx, |panel, window, cx| {
  556        assert!(panel.filename_editor.read(cx).is_focused(window));
  557    });
  558    assert_eq!(
  559        visible_entries_as_strings(&panel, 0..10, cx),
  560        &[
  561            "v root1",
  562            "    > .git",
  563            "    > a",
  564            "    > b",
  565            "    > C",
  566            "      [EDITOR: '']  <== selected",
  567            "      .dockerignore",
  568            "v root2",
  569            "    > d",
  570            "    > e",
  571        ]
  572    );
  573
  574    let confirm = panel.update_in(cx, |panel, window, cx| {
  575        panel.filename_editor.update(cx, |editor, cx| {
  576            editor.set_text("the-new-filename", window, cx)
  577        });
  578        panel.confirm_edit(true, window, cx).unwrap()
  579    });
  580    assert_eq!(
  581        visible_entries_as_strings(&panel, 0..10, cx),
  582        &[
  583            "v root1",
  584            "    > .git",
  585            "    > a",
  586            "    > b",
  587            "    > C",
  588            "      [PROCESSING: 'the-new-filename']  <== selected",
  589            "      .dockerignore",
  590            "v root2",
  591            "    > d",
  592            "    > e",
  593        ]
  594    );
  595
  596    confirm.await.unwrap();
  597    cx.run_until_parked();
  598    assert_eq!(
  599        visible_entries_as_strings(&panel, 0..10, cx),
  600        &[
  601            "v root1",
  602            "    > .git",
  603            "    > a",
  604            "    > b",
  605            "    > C",
  606            "      .dockerignore",
  607            "      the-new-filename  <== selected  <== marked",
  608            "v root2",
  609            "    > d",
  610            "    > e",
  611        ]
  612    );
  613
  614    select_path(&panel, "root1/b", cx);
  615    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
  616    cx.run_until_parked();
  617    assert_eq!(
  618        visible_entries_as_strings(&panel, 0..10, cx),
  619        &[
  620            "v root1",
  621            "    > .git",
  622            "    > a",
  623            "    v b",
  624            "        > 3",
  625            "        > 4",
  626            "          [EDITOR: '']  <== selected",
  627            "    > C",
  628            "      .dockerignore",
  629            "      the-new-filename",
  630        ]
  631    );
  632
  633    panel
  634        .update_in(cx, |panel, window, cx| {
  635            panel.filename_editor.update(cx, |editor, cx| {
  636                editor.set_text("another-filename.txt", window, cx)
  637            });
  638            panel.confirm_edit(true, window, cx).unwrap()
  639        })
  640        .await
  641        .unwrap();
  642    cx.run_until_parked();
  643    assert_eq!(
  644        visible_entries_as_strings(&panel, 0..10, cx),
  645        &[
  646            "v root1",
  647            "    > .git",
  648            "    > a",
  649            "    v b",
  650            "        > 3",
  651            "        > 4",
  652            "          another-filename.txt  <== selected  <== marked",
  653            "    > C",
  654            "      .dockerignore",
  655            "      the-new-filename",
  656        ]
  657    );
  658
  659    select_path(&panel, "root1/b/another-filename.txt", cx);
  660    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
  661    assert_eq!(
  662        visible_entries_as_strings(&panel, 0..10, cx),
  663        &[
  664            "v root1",
  665            "    > .git",
  666            "    > a",
  667            "    v b",
  668            "        > 3",
  669            "        > 4",
  670            "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
  671            "    > C",
  672            "      .dockerignore",
  673            "      the-new-filename",
  674        ]
  675    );
  676
  677    let confirm = panel.update_in(cx, |panel, window, cx| {
  678        panel.filename_editor.update(cx, |editor, cx| {
  679            let file_name_selections = editor
  680                .selections
  681                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
  682            assert_eq!(
  683                file_name_selections.len(),
  684                1,
  685                "File editing should have a single selection, but got: {file_name_selections:?}"
  686            );
  687            let file_name_selection = &file_name_selections[0];
  688            assert_eq!(
  689                file_name_selection.start,
  690                MultiBufferOffset(0),
  691                "Should select the file name from the start"
  692            );
  693            assert_eq!(
  694                file_name_selection.end,
  695                MultiBufferOffset("another-filename".len()),
  696                "Should not select file extension"
  697            );
  698
  699            editor.set_text("a-different-filename.tar.gz", window, cx)
  700        });
  701        panel.confirm_edit(true, window, cx).unwrap()
  702    });
  703    assert_eq!(
  704        visible_entries_as_strings(&panel, 0..10, cx),
  705        &[
  706            "v root1",
  707            "    > .git",
  708            "    > a",
  709            "    v b",
  710            "        > 3",
  711            "        > 4",
  712            "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
  713            "    > C",
  714            "      .dockerignore",
  715            "      the-new-filename",
  716        ]
  717    );
  718
  719    confirm.await.unwrap();
  720    cx.run_until_parked();
  721    assert_eq!(
  722        visible_entries_as_strings(&panel, 0..10, cx),
  723        &[
  724            "v root1",
  725            "    > .git",
  726            "    > a",
  727            "    v b",
  728            "        > 3",
  729            "        > 4",
  730            "          a-different-filename.tar.gz  <== selected",
  731            "    > C",
  732            "      .dockerignore",
  733            "      the-new-filename",
  734        ]
  735    );
  736
  737    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
  738    assert_eq!(
  739        visible_entries_as_strings(&panel, 0..10, cx),
  740        &[
  741            "v root1",
  742            "    > .git",
  743            "    > a",
  744            "    v b",
  745            "        > 3",
  746            "        > 4",
  747            "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
  748            "    > C",
  749            "      .dockerignore",
  750            "      the-new-filename",
  751        ]
  752    );
  753
  754    panel.update_in(cx, |panel, window, cx| {
  755            panel.filename_editor.update(cx, |editor, cx| {
  756                let file_name_selections = editor.selections.all::<MultiBufferOffset>(&editor.display_snapshot(cx));
  757                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
  758                let file_name_selection = &file_name_selections[0];
  759                assert_eq!(file_name_selection.start, MultiBufferOffset(0), "Should select the file name from the start");
  760                assert_eq!(file_name_selection.end, MultiBufferOffset("a-different-filename.tar".len()), "Should not select file extension, but still may select anything up to the last dot..");
  761
  762            });
  763            panel.cancel(&menu::Cancel, window, cx)
  764        });
  765    cx.run_until_parked();
  766    panel.update_in(cx, |panel, window, cx| {
  767        panel.new_directory(&NewDirectory, window, cx)
  768    });
  769    cx.run_until_parked();
  770    assert_eq!(
  771        visible_entries_as_strings(&panel, 0..10, cx),
  772        &[
  773            "v root1",
  774            "    > .git",
  775            "    > a",
  776            "    v b",
  777            "        > [EDITOR: '']  <== selected",
  778            "        > 3",
  779            "        > 4",
  780            "          a-different-filename.tar.gz",
  781            "    > C",
  782            "      .dockerignore",
  783        ]
  784    );
  785
  786    let confirm = panel.update_in(cx, |panel, window, cx| {
  787        panel
  788            .filename_editor
  789            .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
  790        panel.confirm_edit(true, window, cx).unwrap()
  791    });
  792    panel.update_in(cx, |panel, window, cx| {
  793        panel.select_next(&Default::default(), window, cx)
  794    });
  795    assert_eq!(
  796        visible_entries_as_strings(&panel, 0..10, cx),
  797        &[
  798            "v root1",
  799            "    > .git",
  800            "    > a",
  801            "    v b",
  802            "        > [PROCESSING: 'new-dir']",
  803            "        > 3  <== selected",
  804            "        > 4",
  805            "          a-different-filename.tar.gz",
  806            "    > C",
  807            "      .dockerignore",
  808        ]
  809    );
  810
  811    confirm.await.unwrap();
  812    cx.run_until_parked();
  813    assert_eq!(
  814        visible_entries_as_strings(&panel, 0..10, cx),
  815        &[
  816            "v root1",
  817            "    > .git",
  818            "    > a",
  819            "    v b",
  820            "        > 3  <== selected",
  821            "        > 4",
  822            "        > new-dir",
  823            "          a-different-filename.tar.gz",
  824            "    > C",
  825            "      .dockerignore",
  826        ]
  827    );
  828
  829    panel.update_in(cx, |panel, window, cx| {
  830        panel.rename(&Default::default(), window, cx)
  831    });
  832    cx.run_until_parked();
  833    assert_eq!(
  834        visible_entries_as_strings(&panel, 0..10, cx),
  835        &[
  836            "v root1",
  837            "    > .git",
  838            "    > a",
  839            "    v b",
  840            "        > [EDITOR: '3']  <== selected",
  841            "        > 4",
  842            "        > new-dir",
  843            "          a-different-filename.tar.gz",
  844            "    > C",
  845            "      .dockerignore",
  846        ]
  847    );
  848
  849    // Dismiss the rename editor when it loses focus.
  850    workspace.update_in(cx, |_, window, _| window.blur());
  851    assert_eq!(
  852        visible_entries_as_strings(&panel, 0..10, cx),
  853        &[
  854            "v root1",
  855            "    > .git",
  856            "    > a",
  857            "    v b",
  858            "        > 3  <== selected",
  859            "        > 4",
  860            "        > new-dir",
  861            "          a-different-filename.tar.gz",
  862            "    > C",
  863            "      .dockerignore",
  864        ]
  865    );
  866
  867    // Test empty filename and filename with only whitespace
  868    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
  869    cx.run_until_parked();
  870    assert_eq!(
  871        visible_entries_as_strings(&panel, 0..10, cx),
  872        &[
  873            "v root1",
  874            "    > .git",
  875            "    > a",
  876            "    v b",
  877            "        v 3",
  878            "              [EDITOR: '']  <== selected",
  879            "              Q",
  880            "        > 4",
  881            "        > new-dir",
  882            "          a-different-filename.tar.gz",
  883        ]
  884    );
  885    panel.update_in(cx, |panel, window, cx| {
  886        panel.filename_editor.update(cx, |editor, cx| {
  887            editor.set_text("", window, cx);
  888        });
  889        assert!(panel.confirm_edit(true, window, cx).is_none());
  890        panel.filename_editor.update(cx, |editor, cx| {
  891            editor.set_text("   ", window, cx);
  892        });
  893        assert!(panel.confirm_edit(true, window, cx).is_none());
  894        panel.cancel(&menu::Cancel, window, cx);
  895    });
  896    cx.run_until_parked();
  897    assert_eq!(
  898        visible_entries_as_strings(&panel, 0..10, cx),
  899        &[
  900            "v root1",
  901            "    > .git",
  902            "    > a",
  903            "    v b",
  904            "        v 3  <== selected",
  905            "              Q",
  906            "        > 4",
  907            "        > new-dir",
  908            "          a-different-filename.tar.gz",
  909            "    > C",
  910        ]
  911    );
  912}
  913
  914#[gpui::test(iterations = 10)]
  915async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
  916    init_test(cx);
  917
  918    let fs = FakeFs::new(cx.executor());
  919    fs.insert_tree(
  920        "/root1",
  921        json!({
  922            ".dockerignore": "",
  923            ".git": {
  924                "HEAD": "",
  925            },
  926            "a": {
  927                "0": { "q": "", "r": "", "s": "" },
  928                "1": { "t": "", "u": "" },
  929                "2": { "v": "", "w": "", "x": "", "y": "" },
  930            },
  931            "b": {
  932                "3": { "Q": "" },
  933                "4": { "R": "", "S": "", "T": "", "U": "" },
  934            },
  935            "C": {
  936                "5": {},
  937                "6": { "V": "", "W": "" },
  938                "7": { "X": "" },
  939                "8": { "Y": {}, "Z": "" }
  940            }
  941        }),
  942    )
  943    .await;
  944    fs.insert_tree(
  945        "/root2",
  946        json!({
  947            "d": {
  948                "9": ""
  949            },
  950            "e": {}
  951        }),
  952    )
  953    .await;
  954
  955    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
  956    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  957    let workspace = window
  958        .read_with(cx, |mw, _| mw.workspace().clone())
  959        .unwrap();
  960    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  961    let panel = workspace.update_in(cx, |workspace, window, cx| {
  962        let panel = ProjectPanel::new(workspace, window, cx);
  963        workspace.add_panel(panel.clone(), window, cx);
  964        panel
  965    });
  966    cx.run_until_parked();
  967
  968    select_path(&panel, "root1", cx);
  969    assert_eq!(
  970        visible_entries_as_strings(&panel, 0..10, cx),
  971        &[
  972            "v root1  <== selected",
  973            "    > .git",
  974            "    > a",
  975            "    > b",
  976            "    > C",
  977            "      .dockerignore",
  978            "v root2",
  979            "    > d",
  980            "    > e",
  981        ]
  982    );
  983
  984    // Add a file with the root folder selected. The filename editor is placed
  985    // before the first file in the root folder.
  986    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
  987    cx.run_until_parked();
  988    panel.update_in(cx, |panel, window, cx| {
  989        assert!(panel.filename_editor.read(cx).is_focused(window));
  990    });
  991    cx.run_until_parked();
  992    assert_eq!(
  993        visible_entries_as_strings(&panel, 0..10, cx),
  994        &[
  995            "v root1",
  996            "    > .git",
  997            "    > a",
  998            "    > b",
  999            "    > C",
 1000            "      [EDITOR: '']  <== selected",
 1001            "      .dockerignore",
 1002            "v root2",
 1003            "    > d",
 1004            "    > e",
 1005        ]
 1006    );
 1007
 1008    let confirm = panel.update_in(cx, |panel, window, cx| {
 1009        panel.filename_editor.update(cx, |editor, cx| {
 1010            editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
 1011        });
 1012        panel.confirm_edit(true, window, cx).unwrap()
 1013    });
 1014
 1015    assert_eq!(
 1016        visible_entries_as_strings(&panel, 0..10, cx),
 1017        &[
 1018            "v root1",
 1019            "    > .git",
 1020            "    > a",
 1021            "    > b",
 1022            "    > C",
 1023            "      [PROCESSING: 'bdir1/dir2/the-new-filename']  <== selected",
 1024            "      .dockerignore",
 1025            "v root2",
 1026            "    > d",
 1027            "    > e",
 1028        ]
 1029    );
 1030
 1031    confirm.await.unwrap();
 1032    cx.run_until_parked();
 1033    assert_eq!(
 1034        visible_entries_as_strings(&panel, 0..13, cx),
 1035        &[
 1036            "v root1",
 1037            "    > .git",
 1038            "    > a",
 1039            "    > b",
 1040            "    v bdir1",
 1041            "        v dir2",
 1042            "              the-new-filename  <== selected  <== marked",
 1043            "    > C",
 1044            "      .dockerignore",
 1045            "v root2",
 1046            "    > d",
 1047            "    > e",
 1048        ]
 1049    );
 1050}
 1051
 1052#[gpui::test]
 1053async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
 1054    init_test(cx);
 1055
 1056    let fs = FakeFs::new(cx.executor());
 1057    fs.insert_tree(
 1058        path!("/root1"),
 1059        json!({
 1060            ".dockerignore": "",
 1061            ".git": {
 1062                "HEAD": "",
 1063            },
 1064        }),
 1065    )
 1066    .await;
 1067
 1068    let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
 1069    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1070    let workspace = window
 1071        .read_with(cx, |mw, _| mw.workspace().clone())
 1072        .unwrap();
 1073    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1074    let panel = workspace.update_in(cx, |workspace, window, cx| {
 1075        let panel = ProjectPanel::new(workspace, window, cx);
 1076        workspace.add_panel(panel.clone(), window, cx);
 1077        panel
 1078    });
 1079    cx.run_until_parked();
 1080
 1081    select_path(&panel, "root1", cx);
 1082    assert_eq!(
 1083        visible_entries_as_strings(&panel, 0..10, cx),
 1084        &["v root1  <== selected", "    > .git", "      .dockerignore",]
 1085    );
 1086
 1087    // Add a file with the root folder selected. The filename editor is placed
 1088    // before the first file in the root folder.
 1089    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 1090    cx.run_until_parked();
 1091    panel.update_in(cx, |panel, window, cx| {
 1092        assert!(panel.filename_editor.read(cx).is_focused(window));
 1093    });
 1094    assert_eq!(
 1095        visible_entries_as_strings(&panel, 0..10, cx),
 1096        &[
 1097            "v root1",
 1098            "    > .git",
 1099            "      [EDITOR: '']  <== selected",
 1100            "      .dockerignore",
 1101        ]
 1102    );
 1103
 1104    let confirm = panel.update_in(cx, |panel, window, cx| {
 1105        // If we want to create a subdirectory, there should be no prefix slash.
 1106        panel
 1107            .filename_editor
 1108            .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
 1109        panel.confirm_edit(true, window, cx).unwrap()
 1110    });
 1111
 1112    assert_eq!(
 1113        visible_entries_as_strings(&panel, 0..10, cx),
 1114        &[
 1115            "v root1",
 1116            "    > .git",
 1117            "      [PROCESSING: 'new_dir']  <== selected",
 1118            "      .dockerignore",
 1119        ]
 1120    );
 1121
 1122    confirm.await.unwrap();
 1123    cx.run_until_parked();
 1124    assert_eq!(
 1125        visible_entries_as_strings(&panel, 0..10, cx),
 1126        &[
 1127            "v root1",
 1128            "    > .git",
 1129            "    v new_dir  <== selected",
 1130            "      .dockerignore",
 1131        ]
 1132    );
 1133
 1134    // Test filename with whitespace
 1135    select_path(&panel, "root1", cx);
 1136    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 1137    let confirm = panel.update_in(cx, |panel, window, cx| {
 1138        // If we want to create a subdirectory, there should be no prefix slash.
 1139        panel
 1140            .filename_editor
 1141            .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
 1142        panel.confirm_edit(true, window, cx).unwrap()
 1143    });
 1144    confirm.await.unwrap();
 1145    cx.run_until_parked();
 1146    assert_eq!(
 1147        visible_entries_as_strings(&panel, 0..10, cx),
 1148        &[
 1149            "v root1",
 1150            "    > .git",
 1151            "    v new dir 2  <== selected",
 1152            "    v new_dir",
 1153            "      .dockerignore",
 1154        ]
 1155    );
 1156
 1157    // Test filename ends with "\"
 1158    #[cfg(target_os = "windows")]
 1159    {
 1160        select_path(&panel, "root1", cx);
 1161        panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 1162        let confirm = panel.update_in(cx, |panel, window, cx| {
 1163            // If we want to create a subdirectory, there should be no prefix slash.
 1164            panel
 1165                .filename_editor
 1166                .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
 1167            panel.confirm_edit(true, window, cx).unwrap()
 1168        });
 1169        confirm.await.unwrap();
 1170        cx.run_until_parked();
 1171        assert_eq!(
 1172            visible_entries_as_strings(&panel, 0..10, cx),
 1173            &[
 1174                "v root1",
 1175                "    > .git",
 1176                "    v new dir 2",
 1177                "    v new_dir",
 1178                "    v new_dir_3  <== selected",
 1179                "      .dockerignore",
 1180            ]
 1181        );
 1182    }
 1183}
 1184
 1185#[gpui::test]
 1186async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
 1187    init_test(cx);
 1188
 1189    let fs = FakeFs::new(cx.executor());
 1190    fs.insert_tree(
 1191        "/root1",
 1192        json!({
 1193            "one.two.txt": "",
 1194            "one.txt": ""
 1195        }),
 1196    )
 1197    .await;
 1198
 1199    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 1200    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1201    let workspace = window
 1202        .read_with(cx, |mw, _| mw.workspace().clone())
 1203        .unwrap();
 1204    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1205    let panel = workspace.update_in(cx, ProjectPanel::new);
 1206    cx.run_until_parked();
 1207
 1208    panel.update_in(cx, |panel, window, cx| {
 1209        panel.select_next(&Default::default(), window, cx);
 1210        panel.select_next(&Default::default(), window, cx);
 1211    });
 1212
 1213    assert_eq!(
 1214        visible_entries_as_strings(&panel, 0..50, cx),
 1215        &[
 1216            //
 1217            "v root1",
 1218            "      one.txt  <== selected",
 1219            "      one.two.txt",
 1220        ]
 1221    );
 1222
 1223    // Regression test - file name is created correctly when
 1224    // the copied file's name contains multiple dots.
 1225    panel.update_in(cx, |panel, window, cx| {
 1226        panel.copy(&Default::default(), window, cx);
 1227        panel.paste(&Default::default(), window, cx);
 1228    });
 1229    cx.executor().run_until_parked();
 1230    panel.update_in(cx, |panel, window, cx| {
 1231        assert!(panel.filename_editor.read(cx).is_focused(window));
 1232    });
 1233    assert_eq!(
 1234        visible_entries_as_strings(&panel, 0..50, cx),
 1235        &[
 1236            //
 1237            "v root1",
 1238            "      one.txt",
 1239            "      [EDITOR: 'one copy.txt']  <== selected  <== marked",
 1240            "      one.two.txt",
 1241        ]
 1242    );
 1243
 1244    panel.update_in(cx, |panel, window, cx| {
 1245        panel.filename_editor.update(cx, |editor, cx| {
 1246            let file_name_selections = editor
 1247                .selections
 1248                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
 1249            assert_eq!(
 1250                file_name_selections.len(),
 1251                1,
 1252                "File editing should have a single selection, but got: {file_name_selections:?}"
 1253            );
 1254            let file_name_selection = &file_name_selections[0];
 1255            assert_eq!(
 1256                file_name_selection.start,
 1257                MultiBufferOffset("one".len()),
 1258                "Should select the file name disambiguation after the original file name"
 1259            );
 1260            assert_eq!(
 1261                file_name_selection.end,
 1262                MultiBufferOffset("one copy".len()),
 1263                "Should select the file name disambiguation until the extension"
 1264            );
 1265        });
 1266        assert!(panel.confirm_edit(true, window, cx).is_none());
 1267    });
 1268
 1269    panel.update_in(cx, |panel, window, cx| {
 1270        panel.paste(&Default::default(), window, cx);
 1271    });
 1272    cx.executor().run_until_parked();
 1273    panel.update_in(cx, |panel, window, cx| {
 1274        assert!(panel.filename_editor.read(cx).is_focused(window));
 1275    });
 1276    assert_eq!(
 1277        visible_entries_as_strings(&panel, 0..50, cx),
 1278        &[
 1279            //
 1280            "v root1",
 1281            "      one.txt",
 1282            "      one copy.txt",
 1283            "      [EDITOR: 'one copy 1.txt']  <== selected  <== marked",
 1284            "      one.two.txt",
 1285        ]
 1286    );
 1287
 1288    panel.update_in(cx, |panel, window, cx| {
 1289        assert!(panel.confirm_edit(true, window, cx).is_none())
 1290    });
 1291}
 1292
 1293#[gpui::test]
 1294async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
 1295    init_test(cx);
 1296
 1297    let fs = FakeFs::new(cx.executor());
 1298    fs.insert_tree(
 1299        "/root",
 1300        json!({
 1301            "one.txt": "",
 1302            "two.txt": "",
 1303            "a": {},
 1304            "b": {}
 1305        }),
 1306    )
 1307    .await;
 1308
 1309    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 1310    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1311    let workspace = window
 1312        .read_with(cx, |mw, _| mw.workspace().clone())
 1313        .unwrap();
 1314    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1315    let panel = workspace.update_in(cx, ProjectPanel::new);
 1316    cx.run_until_parked();
 1317
 1318    select_path_with_mark(&panel, "root/one.txt", cx);
 1319    select_path_with_mark(&panel, "root/two.txt", cx);
 1320
 1321    assert_eq!(
 1322        visible_entries_as_strings(&panel, 0..50, cx),
 1323        &[
 1324            "v root",
 1325            "    > a",
 1326            "    > b",
 1327            "      one.txt  <== marked",
 1328            "      two.txt  <== selected  <== marked",
 1329        ]
 1330    );
 1331
 1332    panel.update_in(cx, |panel, window, cx| {
 1333        panel.cut(&Default::default(), window, cx);
 1334    });
 1335
 1336    select_path(&panel, "root/a", cx);
 1337
 1338    panel.update_in(cx, |panel, window, cx| {
 1339        panel.paste(&Default::default(), window, cx);
 1340        panel.update_visible_entries(None, false, false, window, cx);
 1341    });
 1342    cx.executor().run_until_parked();
 1343
 1344    assert_eq!(
 1345        visible_entries_as_strings(&panel, 0..50, cx),
 1346        &[
 1347            "v root",
 1348            "    v a",
 1349            "          one.txt  <== marked",
 1350            "          two.txt  <== selected  <== marked",
 1351            "    > b",
 1352        ],
 1353        "Cut entries should be moved on first paste."
 1354    );
 1355
 1356    panel.update_in(cx, |panel, window, cx| {
 1357        panel.cancel(&menu::Cancel {}, window, cx)
 1358    });
 1359    cx.executor().run_until_parked();
 1360
 1361    select_path(&panel, "root/b", cx);
 1362
 1363    panel.update_in(cx, |panel, window, cx| {
 1364        panel.paste(&Default::default(), window, cx);
 1365    });
 1366    cx.executor().run_until_parked();
 1367
 1368    assert_eq!(
 1369        visible_entries_as_strings(&panel, 0..50, cx),
 1370        &[
 1371            "v root",
 1372            "    v a",
 1373            "          one.txt",
 1374            "          two.txt",
 1375            "    v b",
 1376            "          one.txt",
 1377            "          two.txt  <== selected",
 1378        ],
 1379        "Cut entries should only be copied for the second paste!"
 1380    );
 1381}
 1382
 1383#[gpui::test]
 1384async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
 1385    init_test(cx);
 1386
 1387    let fs = FakeFs::new(cx.executor());
 1388    fs.insert_tree(
 1389        "/root1",
 1390        json!({
 1391            "one.txt": "",
 1392            "two.txt": "",
 1393            "three.txt": "",
 1394            "a": {
 1395                "0": { "q": "", "r": "", "s": "" },
 1396                "1": { "t": "", "u": "" },
 1397                "2": { "v": "", "w": "", "x": "", "y": "" },
 1398            },
 1399        }),
 1400    )
 1401    .await;
 1402
 1403    fs.insert_tree(
 1404        "/root2",
 1405        json!({
 1406            "one.txt": "",
 1407            "two.txt": "",
 1408            "four.txt": "",
 1409            "b": {
 1410                "3": { "Q": "" },
 1411                "4": { "R": "", "S": "", "T": "", "U": "" },
 1412            },
 1413        }),
 1414    )
 1415    .await;
 1416
 1417    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 1418    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1419    let workspace = window
 1420        .read_with(cx, |mw, _| mw.workspace().clone())
 1421        .unwrap();
 1422    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1423    let panel = workspace.update_in(cx, ProjectPanel::new);
 1424    cx.run_until_parked();
 1425
 1426    select_path(&panel, "root1/three.txt", cx);
 1427    panel.update_in(cx, |panel, window, cx| {
 1428        panel.cut(&Default::default(), window, cx);
 1429    });
 1430
 1431    select_path(&panel, "root2/one.txt", cx);
 1432    panel.update_in(cx, |panel, window, cx| {
 1433        panel.select_next(&Default::default(), window, cx);
 1434        panel.paste(&Default::default(), window, cx);
 1435    });
 1436    cx.executor().run_until_parked();
 1437    assert_eq!(
 1438        visible_entries_as_strings(&panel, 0..50, cx),
 1439        &[
 1440            //
 1441            "v root1",
 1442            "    > a",
 1443            "      one.txt",
 1444            "      two.txt",
 1445            "v root2",
 1446            "    > b",
 1447            "      four.txt",
 1448            "      one.txt",
 1449            "      three.txt  <== selected  <== marked",
 1450            "      two.txt",
 1451        ]
 1452    );
 1453
 1454    select_path(&panel, "root1/a", cx);
 1455    panel.update_in(cx, |panel, window, cx| {
 1456        panel.cut(&Default::default(), window, cx);
 1457    });
 1458    select_path(&panel, "root2/two.txt", cx);
 1459    panel.update_in(cx, |panel, window, cx| {
 1460        panel.select_next(&Default::default(), window, cx);
 1461        panel.paste(&Default::default(), window, cx);
 1462    });
 1463
 1464    cx.executor().run_until_parked();
 1465    assert_eq!(
 1466        visible_entries_as_strings(&panel, 0..50, cx),
 1467        &[
 1468            //
 1469            "v root1",
 1470            "      one.txt",
 1471            "      two.txt",
 1472            "v root2",
 1473            "    > a  <== selected",
 1474            "    > b",
 1475            "      four.txt",
 1476            "      one.txt",
 1477            "      three.txt  <== marked",
 1478            "      two.txt",
 1479        ]
 1480    );
 1481}
 1482
 1483#[gpui::test]
 1484async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
 1485    init_test(cx);
 1486
 1487    let fs = FakeFs::new(cx.executor());
 1488    fs.insert_tree(
 1489        "/root1",
 1490        json!({
 1491            "one.txt": "",
 1492            "two.txt": "",
 1493            "three.txt": "",
 1494            "a": {
 1495                "0": { "q": "", "r": "", "s": "" },
 1496                "1": { "t": "", "u": "" },
 1497                "2": { "v": "", "w": "", "x": "", "y": "" },
 1498            },
 1499        }),
 1500    )
 1501    .await;
 1502
 1503    fs.insert_tree(
 1504        "/root2",
 1505        json!({
 1506            "one.txt": "",
 1507            "two.txt": "",
 1508            "four.txt": "",
 1509            "b": {
 1510                "3": { "Q": "" },
 1511                "4": { "R": "", "S": "", "T": "", "U": "" },
 1512            },
 1513        }),
 1514    )
 1515    .await;
 1516
 1517    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 1518    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1519    let workspace = window
 1520        .read_with(cx, |mw, _| mw.workspace().clone())
 1521        .unwrap();
 1522    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1523    let panel = workspace.update_in(cx, ProjectPanel::new);
 1524    cx.run_until_parked();
 1525
 1526    select_path(&panel, "root1/three.txt", cx);
 1527    panel.update_in(cx, |panel, window, cx| {
 1528        panel.copy(&Default::default(), window, cx);
 1529    });
 1530
 1531    select_path(&panel, "root2/one.txt", cx);
 1532    panel.update_in(cx, |panel, window, cx| {
 1533        panel.select_next(&Default::default(), window, cx);
 1534        panel.paste(&Default::default(), window, cx);
 1535    });
 1536    cx.executor().run_until_parked();
 1537    assert_eq!(
 1538        visible_entries_as_strings(&panel, 0..50, cx),
 1539        &[
 1540            //
 1541            "v root1",
 1542            "    > a",
 1543            "      one.txt",
 1544            "      three.txt",
 1545            "      two.txt",
 1546            "v root2",
 1547            "    > b",
 1548            "      four.txt",
 1549            "      one.txt",
 1550            "      three.txt  <== selected  <== marked",
 1551            "      two.txt",
 1552        ]
 1553    );
 1554
 1555    select_path(&panel, "root1/three.txt", cx);
 1556    panel.update_in(cx, |panel, window, cx| {
 1557        panel.copy(&Default::default(), window, cx);
 1558    });
 1559    select_path(&panel, "root2/two.txt", cx);
 1560    panel.update_in(cx, |panel, window, cx| {
 1561        panel.select_next(&Default::default(), window, cx);
 1562        panel.paste(&Default::default(), window, cx);
 1563    });
 1564
 1565    cx.executor().run_until_parked();
 1566    assert_eq!(
 1567        visible_entries_as_strings(&panel, 0..50, cx),
 1568        &[
 1569            //
 1570            "v root1",
 1571            "    > a",
 1572            "      one.txt",
 1573            "      three.txt",
 1574            "      two.txt",
 1575            "v root2",
 1576            "    > b",
 1577            "      four.txt",
 1578            "      one.txt",
 1579            "      three.txt",
 1580            "      [EDITOR: 'three copy.txt']  <== selected  <== marked",
 1581            "      two.txt",
 1582        ]
 1583    );
 1584
 1585    panel.update_in(cx, |panel, window, cx| {
 1586        panel.cancel(&menu::Cancel {}, window, cx)
 1587    });
 1588    cx.executor().run_until_parked();
 1589
 1590    select_path(&panel, "root1/a", cx);
 1591    panel.update_in(cx, |panel, window, cx| {
 1592        panel.copy(&Default::default(), window, cx);
 1593    });
 1594    select_path(&panel, "root2/two.txt", cx);
 1595    panel.update_in(cx, |panel, window, cx| {
 1596        panel.select_next(&Default::default(), window, cx);
 1597        panel.paste(&Default::default(), window, cx);
 1598    });
 1599
 1600    cx.executor().run_until_parked();
 1601    assert_eq!(
 1602        visible_entries_as_strings(&panel, 0..50, cx),
 1603        &[
 1604            //
 1605            "v root1",
 1606            "    > a",
 1607            "      one.txt",
 1608            "      three.txt",
 1609            "      two.txt",
 1610            "v root2",
 1611            "    > a  <== selected",
 1612            "    > b",
 1613            "      four.txt",
 1614            "      one.txt",
 1615            "      three.txt",
 1616            "      three copy.txt",
 1617            "      two.txt",
 1618        ]
 1619    );
 1620}
 1621
 1622#[gpui::test]
 1623async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
 1624    init_test(cx);
 1625
 1626    let fs = FakeFs::new(cx.executor());
 1627    fs.insert_tree(
 1628        "/root",
 1629        json!({
 1630            "a": {
 1631                "one.txt": "",
 1632                "two.txt": "",
 1633                "inner_dir": {
 1634                    "three.txt": "",
 1635                    "four.txt": "",
 1636                }
 1637            },
 1638            "b": {},
 1639            "d.1.20": {
 1640                "default.conf": "",
 1641            }
 1642        }),
 1643    )
 1644    .await;
 1645
 1646    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 1647    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1648    let workspace = window
 1649        .read_with(cx, |mw, _| mw.workspace().clone())
 1650        .unwrap();
 1651    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1652    let panel = workspace.update_in(cx, ProjectPanel::new);
 1653    cx.run_until_parked();
 1654
 1655    select_path(&panel, "root/a", cx);
 1656    panel.update_in(cx, |panel, window, cx| {
 1657        panel.copy(&Default::default(), window, cx);
 1658        panel.select_next(&Default::default(), window, cx);
 1659        panel.paste(&Default::default(), window, cx);
 1660    });
 1661    cx.executor().run_until_parked();
 1662
 1663    let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
 1664    assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
 1665
 1666    let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
 1667    assert_ne!(
 1668        pasted_dir_file, None,
 1669        "Pasted directory file should have an entry"
 1670    );
 1671
 1672    let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
 1673    assert_ne!(
 1674        pasted_dir_inner_dir, None,
 1675        "Directories inside pasted directory should have an entry"
 1676    );
 1677
 1678    toggle_expand_dir(&panel, "root/b/a", cx);
 1679    toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
 1680
 1681    assert_eq!(
 1682        visible_entries_as_strings(&panel, 0..50, cx),
 1683        &[
 1684            //
 1685            "v root",
 1686            "    > a",
 1687            "    v b",
 1688            "        v a",
 1689            "            v inner_dir  <== selected",
 1690            "                  four.txt",
 1691            "                  three.txt",
 1692            "              one.txt",
 1693            "              two.txt",
 1694            "    > d.1.20",
 1695        ]
 1696    );
 1697
 1698    select_path(&panel, "root", cx);
 1699    panel.update_in(cx, |panel, window, cx| {
 1700        panel.paste(&Default::default(), window, cx)
 1701    });
 1702    cx.executor().run_until_parked();
 1703    assert_eq!(
 1704        visible_entries_as_strings(&panel, 0..50, cx),
 1705        &[
 1706            //
 1707            "v root",
 1708            "    > a",
 1709            "    > [EDITOR: 'a copy']  <== selected",
 1710            "    v b",
 1711            "        v a",
 1712            "            v inner_dir",
 1713            "                  four.txt",
 1714            "                  three.txt",
 1715            "              one.txt",
 1716            "              two.txt",
 1717            "    > d.1.20",
 1718        ]
 1719    );
 1720
 1721    let confirm = panel.update_in(cx, |panel, window, cx| {
 1722        panel
 1723            .filename_editor
 1724            .update(cx, |editor, cx| editor.set_text("c", window, cx));
 1725        panel.confirm_edit(true, window, cx).unwrap()
 1726    });
 1727    assert_eq!(
 1728        visible_entries_as_strings(&panel, 0..50, cx),
 1729        &[
 1730            //
 1731            "v root",
 1732            "    > a",
 1733            "    > [PROCESSING: 'c']  <== selected",
 1734            "    v b",
 1735            "        v a",
 1736            "            v inner_dir",
 1737            "                  four.txt",
 1738            "                  three.txt",
 1739            "              one.txt",
 1740            "              two.txt",
 1741            "    > d.1.20",
 1742        ]
 1743    );
 1744
 1745    confirm.await.unwrap();
 1746
 1747    panel.update_in(cx, |panel, window, cx| {
 1748        panel.paste(&Default::default(), window, cx)
 1749    });
 1750    cx.executor().run_until_parked();
 1751    assert_eq!(
 1752        visible_entries_as_strings(&panel, 0..50, cx),
 1753        &[
 1754            //
 1755            "v root",
 1756            "    > a",
 1757            "    v b",
 1758            "        v a",
 1759            "            v inner_dir",
 1760            "                  four.txt",
 1761            "                  three.txt",
 1762            "              one.txt",
 1763            "              two.txt",
 1764            "    v c",
 1765            "        > a  <== selected",
 1766            "        > inner_dir",
 1767            "          one.txt",
 1768            "          two.txt",
 1769            "    > d.1.20",
 1770        ]
 1771    );
 1772
 1773    select_path(&panel, "root/d.1.20", cx);
 1774    panel.update_in(cx, |panel, window, cx| {
 1775        panel.copy(&Default::default(), window, cx);
 1776        panel.paste(&Default::default(), window, cx);
 1777    });
 1778    cx.executor().run_until_parked();
 1779    assert_eq!(
 1780        visible_entries_as_strings(&panel, 0..50, cx),
 1781        &[
 1782            //
 1783            "v root",
 1784            "    > a",
 1785            "    v b",
 1786            "        v a",
 1787            "            v inner_dir",
 1788            "                  four.txt",
 1789            "                  three.txt",
 1790            "              one.txt",
 1791            "              two.txt",
 1792            "    v c",
 1793            "        > a",
 1794            "        > inner_dir",
 1795            "          one.txt",
 1796            "          two.txt",
 1797            "    v d.1.20",
 1798            "          default.conf",
 1799            "    > [EDITOR: 'd.1.20 copy']  <== selected",
 1800        ],
 1801        "Dotted directory names should not be split at the dot when disambiguating"
 1802    );
 1803}
 1804
 1805#[gpui::test]
 1806async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
 1807    init_test(cx);
 1808
 1809    let fs = FakeFs::new(cx.executor());
 1810    fs.insert_tree(
 1811        "/test",
 1812        json!({
 1813            "dir1": {
 1814                "a.txt": "",
 1815                "b.txt": "",
 1816            },
 1817            "dir2": {},
 1818            "c.txt": "",
 1819            "d.txt": "",
 1820        }),
 1821    )
 1822    .await;
 1823
 1824    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 1825    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1826    let workspace = window
 1827        .read_with(cx, |mw, _| mw.workspace().clone())
 1828        .unwrap();
 1829    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1830    let panel = workspace.update_in(cx, ProjectPanel::new);
 1831    cx.run_until_parked();
 1832
 1833    toggle_expand_dir(&panel, "test/dir1", cx);
 1834
 1835    cx.simulate_modifiers_change(gpui::Modifiers {
 1836        control: true,
 1837        ..Default::default()
 1838    });
 1839
 1840    select_path_with_mark(&panel, "test/dir1", cx);
 1841    select_path_with_mark(&panel, "test/c.txt", cx);
 1842
 1843    assert_eq!(
 1844        visible_entries_as_strings(&panel, 0..15, cx),
 1845        &[
 1846            "v test",
 1847            "    v dir1  <== marked",
 1848            "          a.txt",
 1849            "          b.txt",
 1850            "    > dir2",
 1851            "      c.txt  <== selected  <== marked",
 1852            "      d.txt",
 1853        ],
 1854        "Initial state before copying dir1 and c.txt"
 1855    );
 1856
 1857    panel.update_in(cx, |panel, window, cx| {
 1858        panel.copy(&Default::default(), window, cx);
 1859    });
 1860    select_path(&panel, "test/dir2", cx);
 1861    panel.update_in(cx, |panel, window, cx| {
 1862        panel.paste(&Default::default(), window, cx);
 1863    });
 1864    cx.executor().run_until_parked();
 1865
 1866    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
 1867
 1868    assert_eq!(
 1869        visible_entries_as_strings(&panel, 0..15, cx),
 1870        &[
 1871            "v test",
 1872            "    v dir1  <== marked",
 1873            "          a.txt",
 1874            "          b.txt",
 1875            "    v dir2",
 1876            "        v dir1  <== selected",
 1877            "              a.txt",
 1878            "              b.txt",
 1879            "          c.txt",
 1880            "      c.txt  <== marked",
 1881            "      d.txt",
 1882        ],
 1883        "Should copy dir1 as well as c.txt into dir2"
 1884    );
 1885
 1886    // Disambiguating multiple files should not open the rename editor.
 1887    select_path(&panel, "test/dir2", cx);
 1888    panel.update_in(cx, |panel, window, cx| {
 1889        panel.paste(&Default::default(), window, cx);
 1890    });
 1891    cx.executor().run_until_parked();
 1892
 1893    assert_eq!(
 1894        visible_entries_as_strings(&panel, 0..15, cx),
 1895        &[
 1896            "v test",
 1897            "    v dir1  <== marked",
 1898            "          a.txt",
 1899            "          b.txt",
 1900            "    v dir2",
 1901            "        v dir1",
 1902            "              a.txt",
 1903            "              b.txt",
 1904            "        > dir1 copy  <== selected",
 1905            "          c.txt",
 1906            "          c copy.txt",
 1907            "      c.txt  <== marked",
 1908            "      d.txt",
 1909        ],
 1910        "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
 1911    );
 1912}
 1913
 1914#[gpui::test]
 1915async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
 1916    init_test(cx);
 1917
 1918    let fs = FakeFs::new(cx.executor());
 1919    fs.insert_tree(
 1920        "/test",
 1921        json!({
 1922            "dir1": {
 1923                "a.txt": "",
 1924                "b.txt": "",
 1925            },
 1926            "dir2": {},
 1927            "c.txt": "",
 1928            "d.txt": "",
 1929        }),
 1930    )
 1931    .await;
 1932
 1933    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 1934    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 1935    let workspace = window
 1936        .read_with(cx, |mw, _| mw.workspace().clone())
 1937        .unwrap();
 1938    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 1939    let panel = workspace.update_in(cx, ProjectPanel::new);
 1940    cx.run_until_parked();
 1941
 1942    toggle_expand_dir(&panel, "test/dir1", cx);
 1943
 1944    cx.simulate_modifiers_change(gpui::Modifiers {
 1945        control: true,
 1946        ..Default::default()
 1947    });
 1948
 1949    select_path_with_mark(&panel, "test/dir1/a.txt", cx);
 1950    select_path_with_mark(&panel, "test/dir1", cx);
 1951    select_path_with_mark(&panel, "test/c.txt", cx);
 1952
 1953    assert_eq!(
 1954        visible_entries_as_strings(&panel, 0..15, cx),
 1955        &[
 1956            "v test",
 1957            "    v dir1  <== marked",
 1958            "          a.txt  <== marked",
 1959            "          b.txt",
 1960            "    > dir2",
 1961            "      c.txt  <== selected  <== marked",
 1962            "      d.txt",
 1963        ],
 1964        "Initial state before copying a.txt, dir1 and c.txt"
 1965    );
 1966
 1967    panel.update_in(cx, |panel, window, cx| {
 1968        panel.copy(&Default::default(), window, cx);
 1969    });
 1970    select_path(&panel, "test/dir2", cx);
 1971    panel.update_in(cx, |panel, window, cx| {
 1972        panel.paste(&Default::default(), window, cx);
 1973    });
 1974    cx.executor().run_until_parked();
 1975
 1976    toggle_expand_dir(&panel, "test/dir2/dir1", cx);
 1977
 1978    assert_eq!(
 1979        visible_entries_as_strings(&panel, 0..20, cx),
 1980        &[
 1981            "v test",
 1982            "    v dir1  <== marked",
 1983            "          a.txt  <== marked",
 1984            "          b.txt",
 1985            "    v dir2",
 1986            "        v dir1  <== selected",
 1987            "              a.txt",
 1988            "              b.txt",
 1989            "          c.txt",
 1990            "      c.txt  <== marked",
 1991            "      d.txt",
 1992        ],
 1993        "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
 1994    );
 1995}
 1996
 1997#[gpui::test]
 1998async fn test_undo_rename(cx: &mut gpui::TestAppContext) {
 1999    init_test(cx);
 2000
 2001    let fs = FakeFs::new(cx.executor());
 2002    fs.insert_tree(
 2003        "/root",
 2004        json!({
 2005            "a.txt": "",
 2006            "b.txt": "",
 2007        }),
 2008    )
 2009    .await;
 2010
 2011    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2012    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2013    let workspace = window
 2014        .read_with(cx, |mw, _| mw.workspace().clone())
 2015        .unwrap();
 2016    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2017    let panel = workspace.update_in(cx, ProjectPanel::new);
 2018    cx.run_until_parked();
 2019
 2020    select_path(&panel, "root/a.txt", cx);
 2021    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 2022    cx.run_until_parked();
 2023
 2024    let confirm = panel.update_in(cx, |panel, window, cx| {
 2025        panel
 2026            .filename_editor
 2027            .update(cx, |editor, cx| editor.set_text("renamed.txt", window, cx));
 2028        panel.confirm_edit(true, window, cx).unwrap()
 2029    });
 2030    confirm.await.unwrap();
 2031    cx.run_until_parked();
 2032
 2033    assert!(
 2034        find_project_entry(&panel, "root/renamed.txt", cx).is_some(),
 2035        "File should be renamed to renamed.txt"
 2036    );
 2037    assert_eq!(
 2038        find_project_entry(&panel, "root/a.txt", cx),
 2039        None,
 2040        "Original file should no longer exist"
 2041    );
 2042
 2043    panel.update_in(cx, |panel, window, cx| {
 2044        panel.undo(&Undo, window, cx);
 2045    });
 2046    cx.run_until_parked();
 2047
 2048    assert!(
 2049        find_project_entry(&panel, "root/a.txt", cx).is_some(),
 2050        "File should be restored to original name after undo"
 2051    );
 2052    assert_eq!(
 2053        find_project_entry(&panel, "root/renamed.txt", cx),
 2054        None,
 2055        "Renamed file should no longer exist after undo"
 2056    );
 2057}
 2058
 2059#[gpui::test]
 2060async fn test_undo_create_file(cx: &mut gpui::TestAppContext) {
 2061    init_test(cx);
 2062
 2063    let fs = FakeFs::new(cx.executor());
 2064    fs.insert_tree(
 2065        "/root",
 2066        json!({
 2067            "existing.txt": "",
 2068        }),
 2069    )
 2070    .await;
 2071
 2072    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2073    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2074    let workspace = window
 2075        .read_with(cx, |mw, _| mw.workspace().clone())
 2076        .unwrap();
 2077    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2078    let panel = workspace.update_in(cx, ProjectPanel::new);
 2079    cx.run_until_parked();
 2080
 2081    select_path(&panel, "root", cx);
 2082    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 2083    cx.run_until_parked();
 2084
 2085    let confirm = panel.update_in(cx, |panel, window, cx| {
 2086        panel
 2087            .filename_editor
 2088            .update(cx, |editor, cx| editor.set_text("new.txt", window, cx));
 2089        panel.confirm_edit(true, window, cx).unwrap()
 2090    });
 2091    confirm.await.unwrap();
 2092    cx.run_until_parked();
 2093
 2094    assert!(
 2095        find_project_entry(&panel, "root/new.txt", cx).is_some(),
 2096        "New file should exist"
 2097    );
 2098
 2099    panel.update_in(cx, |panel, window, cx| {
 2100        panel.undo(&Undo, window, cx);
 2101    });
 2102    cx.run_until_parked();
 2103
 2104    assert_eq!(
 2105        find_project_entry(&panel, "root/new.txt", cx),
 2106        None,
 2107        "New file should be removed after undo"
 2108    );
 2109    assert!(
 2110        find_project_entry(&panel, "root/existing.txt", cx).is_some(),
 2111        "Existing file should still be present"
 2112    );
 2113}
 2114
 2115#[gpui::test]
 2116async fn test_undo_create_directory(cx: &mut gpui::TestAppContext) {
 2117    init_test(cx);
 2118
 2119    let fs = FakeFs::new(cx.executor());
 2120    fs.insert_tree(
 2121        "/root",
 2122        json!({
 2123            "existing.txt": "",
 2124        }),
 2125    )
 2126    .await;
 2127
 2128    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2129    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2130    let workspace = window
 2131        .read_with(cx, |mw, _| mw.workspace().clone())
 2132        .unwrap();
 2133    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2134    let panel = workspace.update_in(cx, ProjectPanel::new);
 2135    cx.run_until_parked();
 2136
 2137    select_path(&panel, "root", cx);
 2138    panel.update_in(cx, |panel, window, cx| {
 2139        panel.new_directory(&NewDirectory, window, cx)
 2140    });
 2141    cx.run_until_parked();
 2142
 2143    let confirm = panel.update_in(cx, |panel, window, cx| {
 2144        panel
 2145            .filename_editor
 2146            .update(cx, |editor, cx| editor.set_text("new_dir", window, cx));
 2147        panel.confirm_edit(true, window, cx).unwrap()
 2148    });
 2149    confirm.await.unwrap();
 2150    cx.run_until_parked();
 2151
 2152    assert!(
 2153        find_project_entry(&panel, "root/new_dir", cx).is_some(),
 2154        "New directory should exist"
 2155    );
 2156
 2157    panel.update_in(cx, |panel, window, cx| {
 2158        panel.undo(&Undo, window, cx);
 2159    });
 2160    cx.run_until_parked();
 2161
 2162    assert_eq!(
 2163        find_project_entry(&panel, "root/new_dir", cx),
 2164        None,
 2165        "New directory should be removed after undo"
 2166    );
 2167}
 2168
 2169#[gpui::test]
 2170async fn test_undo_cut_paste(cx: &mut gpui::TestAppContext) {
 2171    init_test(cx);
 2172
 2173    let fs = FakeFs::new(cx.executor());
 2174    fs.insert_tree(
 2175        "/root",
 2176        json!({
 2177            "src": {
 2178                "file.txt": "content",
 2179            },
 2180            "dst": {},
 2181        }),
 2182    )
 2183    .await;
 2184
 2185    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2186    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2187    let workspace = window
 2188        .read_with(cx, |mw, _| mw.workspace().clone())
 2189        .unwrap();
 2190    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2191    let panel = workspace.update_in(cx, ProjectPanel::new);
 2192    cx.run_until_parked();
 2193
 2194    toggle_expand_dir(&panel, "root/src", cx);
 2195
 2196    select_path_with_mark(&panel, "root/src/file.txt", cx);
 2197    panel.update_in(cx, |panel, window, cx| {
 2198        panel.cut(&Default::default(), window, cx);
 2199    });
 2200
 2201    select_path(&panel, "root/dst", cx);
 2202    panel.update_in(cx, |panel, window, cx| {
 2203        panel.paste(&Default::default(), window, cx);
 2204    });
 2205    cx.run_until_parked();
 2206
 2207    assert!(
 2208        find_project_entry(&panel, "root/dst/file.txt", cx).is_some(),
 2209        "File should be moved to dst"
 2210    );
 2211    assert_eq!(
 2212        find_project_entry(&panel, "root/src/file.txt", cx),
 2213        None,
 2214        "File should no longer be in src"
 2215    );
 2216
 2217    panel.update_in(cx, |panel, window, cx| {
 2218        panel.undo(&Undo, window, cx);
 2219    });
 2220    cx.run_until_parked();
 2221
 2222    assert!(
 2223        find_project_entry(&panel, "root/src/file.txt", cx).is_some(),
 2224        "File should be back in src after undo"
 2225    );
 2226    assert_eq!(
 2227        find_project_entry(&panel, "root/dst/file.txt", cx),
 2228        None,
 2229        "File should no longer be in dst after undo"
 2230    );
 2231}
 2232
 2233#[gpui::test]
 2234async fn test_undo_drag_single_entry(cx: &mut gpui::TestAppContext) {
 2235    init_test(cx);
 2236
 2237    let fs = FakeFs::new(cx.executor());
 2238    fs.insert_tree(
 2239        "/root",
 2240        json!({
 2241            "src": {
 2242                "main.rs": "",
 2243            },
 2244            "dst": {},
 2245        }),
 2246    )
 2247    .await;
 2248
 2249    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2250    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2251    let workspace = window
 2252        .read_with(cx, |mw, _| mw.workspace().clone())
 2253        .unwrap();
 2254    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2255    let panel = workspace.update_in(cx, ProjectPanel::new);
 2256    cx.run_until_parked();
 2257
 2258    toggle_expand_dir(&panel, "root/src", cx);
 2259
 2260    panel.update(cx, |panel, _| panel.marked_entries.clear());
 2261    select_path_with_mark(&panel, "root/src/main.rs", cx);
 2262    drag_selection_to(&panel, "root/dst", false, cx);
 2263
 2264    assert!(
 2265        find_project_entry(&panel, "root/dst/main.rs", cx).is_some(),
 2266        "File should be in dst after drag"
 2267    );
 2268    assert_eq!(
 2269        find_project_entry(&panel, "root/src/main.rs", cx),
 2270        None,
 2271        "File should no longer be in src after drag"
 2272    );
 2273
 2274    panel.update_in(cx, |panel, window, cx| {
 2275        panel.undo(&Undo, window, cx);
 2276    });
 2277    cx.run_until_parked();
 2278
 2279    assert!(
 2280        find_project_entry(&panel, "root/src/main.rs", cx).is_some(),
 2281        "File should be back in src after undo"
 2282    );
 2283    assert_eq!(
 2284        find_project_entry(&panel, "root/dst/main.rs", cx),
 2285        None,
 2286        "File should no longer be in dst after undo"
 2287    );
 2288}
 2289
 2290#[gpui::test]
 2291async fn test_undo_drag_multiple_entries(cx: &mut gpui::TestAppContext) {
 2292    init_test(cx);
 2293
 2294    let fs = FakeFs::new(cx.executor());
 2295    fs.insert_tree(
 2296        "/root",
 2297        json!({
 2298            "src": {
 2299                "alpha.txt": "",
 2300                "beta.txt": "",
 2301            },
 2302            "dst": {},
 2303        }),
 2304    )
 2305    .await;
 2306
 2307    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2308    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2309    let workspace = window
 2310        .read_with(cx, |mw, _| mw.workspace().clone())
 2311        .unwrap();
 2312    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2313    let panel = workspace.update_in(cx, ProjectPanel::new);
 2314    cx.run_until_parked();
 2315
 2316    toggle_expand_dir(&panel, "root/src", cx);
 2317
 2318    panel.update(cx, |panel, _| panel.marked_entries.clear());
 2319    select_path_with_mark(&panel, "root/src/alpha.txt", cx);
 2320    select_path_with_mark(&panel, "root/src/beta.txt", cx);
 2321    drag_selection_to(&panel, "root/dst", false, cx);
 2322
 2323    assert!(
 2324        find_project_entry(&panel, "root/dst/alpha.txt", cx).is_some(),
 2325        "alpha.txt should be in dst after drag"
 2326    );
 2327    assert!(
 2328        find_project_entry(&panel, "root/dst/beta.txt", cx).is_some(),
 2329        "beta.txt should be in dst after drag"
 2330    );
 2331
 2332    // A single undo should revert the entire batch
 2333    panel.update_in(cx, |panel, window, cx| {
 2334        panel.undo(&Undo, window, cx);
 2335    });
 2336    cx.run_until_parked();
 2337
 2338    assert!(
 2339        find_project_entry(&panel, "root/src/alpha.txt", cx).is_some(),
 2340        "alpha.txt should be back in src after undo"
 2341    );
 2342    assert!(
 2343        find_project_entry(&panel, "root/src/beta.txt", cx).is_some(),
 2344        "beta.txt should be back in src after undo"
 2345    );
 2346    assert_eq!(
 2347        find_project_entry(&panel, "root/dst/alpha.txt", cx),
 2348        None,
 2349        "alpha.txt should no longer be in dst after undo"
 2350    );
 2351    assert_eq!(
 2352        find_project_entry(&panel, "root/dst/beta.txt", cx),
 2353        None,
 2354        "beta.txt should no longer be in dst after undo"
 2355    );
 2356}
 2357
 2358#[gpui::test]
 2359async fn test_multiple_sequential_undos(cx: &mut gpui::TestAppContext) {
 2360    init_test(cx);
 2361
 2362    let fs = FakeFs::new(cx.executor());
 2363    fs.insert_tree(
 2364        "/root",
 2365        json!({
 2366            "a.txt": "",
 2367        }),
 2368    )
 2369    .await;
 2370
 2371    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2372    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2373    let workspace = window
 2374        .read_with(cx, |mw, _| mw.workspace().clone())
 2375        .unwrap();
 2376    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2377    let panel = workspace.update_in(cx, ProjectPanel::new);
 2378    cx.run_until_parked();
 2379
 2380    select_path(&panel, "root/a.txt", cx);
 2381    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 2382    cx.run_until_parked();
 2383    let confirm = panel.update_in(cx, |panel, window, cx| {
 2384        panel
 2385            .filename_editor
 2386            .update(cx, |editor, cx| editor.set_text("b.txt", window, cx));
 2387        panel.confirm_edit(true, window, cx).unwrap()
 2388    });
 2389    confirm.await.unwrap();
 2390    cx.run_until_parked();
 2391
 2392    assert!(find_project_entry(&panel, "root/b.txt", cx).is_some());
 2393
 2394    select_path(&panel, "root", cx);
 2395    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 2396    cx.run_until_parked();
 2397    let confirm = panel.update_in(cx, |panel, window, cx| {
 2398        panel
 2399            .filename_editor
 2400            .update(cx, |editor, cx| editor.set_text("c.txt", window, cx));
 2401        panel.confirm_edit(true, window, cx).unwrap()
 2402    });
 2403    confirm.await.unwrap();
 2404    cx.run_until_parked();
 2405
 2406    assert!(find_project_entry(&panel, "root/b.txt", cx).is_some());
 2407    assert!(find_project_entry(&panel, "root/c.txt", cx).is_some());
 2408
 2409    panel.update_in(cx, |panel, window, cx| {
 2410        panel.undo(&Undo, window, cx);
 2411    });
 2412    cx.run_until_parked();
 2413
 2414    assert_eq!(
 2415        find_project_entry(&panel, "root/c.txt", cx),
 2416        None,
 2417        "c.txt should be removed after first undo"
 2418    );
 2419    assert!(
 2420        find_project_entry(&panel, "root/b.txt", cx).is_some(),
 2421        "b.txt should still exist after first undo"
 2422    );
 2423
 2424    panel.update_in(cx, |panel, window, cx| {
 2425        panel.undo(&Undo, window, cx);
 2426    });
 2427    cx.run_until_parked();
 2428
 2429    assert!(
 2430        find_project_entry(&panel, "root/a.txt", cx).is_some(),
 2431        "a.txt should be restored after second undo"
 2432    );
 2433    assert_eq!(
 2434        find_project_entry(&panel, "root/b.txt", cx),
 2435        None,
 2436        "b.txt should no longer exist after second undo"
 2437    );
 2438}
 2439
 2440#[gpui::test]
 2441async fn test_undo_with_empty_stack(cx: &mut gpui::TestAppContext) {
 2442    init_test(cx);
 2443
 2444    let fs = FakeFs::new(cx.executor());
 2445    fs.insert_tree(
 2446        "/root",
 2447        json!({
 2448            "a.txt": "",
 2449        }),
 2450    )
 2451    .await;
 2452
 2453    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2454    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2455    let workspace = window
 2456        .read_with(cx, |mw, _| mw.workspace().clone())
 2457        .unwrap();
 2458    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2459    let panel = workspace.update_in(cx, ProjectPanel::new);
 2460    cx.run_until_parked();
 2461
 2462    panel.update_in(cx, |panel, window, cx| {
 2463        panel.undo(&Undo, window, cx);
 2464    });
 2465    cx.run_until_parked();
 2466
 2467    assert!(
 2468        find_project_entry(&panel, "root/a.txt", cx).is_some(),
 2469        "File tree should be unchanged after undo on empty stack"
 2470    );
 2471}
 2472
 2473#[gpui::test]
 2474async fn test_undo_batch(cx: &mut gpui::TestAppContext) {
 2475    init_test(cx);
 2476
 2477    let fs = FakeFs::new(cx.executor());
 2478    fs.insert_tree(
 2479        "/root",
 2480        json!({
 2481            "src": {
 2482                "main.rs": "// Code!"
 2483            }
 2484        }),
 2485    )
 2486    .await;
 2487
 2488    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 2489    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2490    let workspace = window
 2491        .read_with(cx, |mw, _| mw.workspace().clone())
 2492        .unwrap();
 2493    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2494    let panel = workspace.update_in(cx, ProjectPanel::new);
 2495    let worktree_id = project.update(cx, |project, cx| {
 2496        project.visible_worktrees(cx).next().unwrap().read(cx).id()
 2497    });
 2498    cx.run_until_parked();
 2499
 2500    // Since there currently isn't a way to both create a folder and the file
 2501    // within it as two separate operations batched under the same
 2502    // `ProjectPanelOperation::Batch` operation, we'll simply record those
 2503    // ourselves, knowing that the filesystem already has the folder and file
 2504    // being provided in the operations.
 2505    panel.update(cx, |panel, _cx| {
 2506        panel.undo_manager.record_batch(vec![
 2507            ProjectPanelOperation::Create {
 2508                project_path: ProjectPath {
 2509                    worktree_id,
 2510                    path: Arc::from(rel_path("src/main.rs")),
 2511                },
 2512            },
 2513            ProjectPanelOperation::Create {
 2514                project_path: ProjectPath {
 2515                    worktree_id,
 2516                    path: Arc::from(rel_path("src/")),
 2517                },
 2518            },
 2519        ]);
 2520    });
 2521
 2522    // Ensure that `src/main.rs` is present in the filesystem before proceeding,
 2523    // otherwise this test is irrelevant.
 2524    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/src/main.rs"))]);
 2525    assert_eq!(
 2526        fs.directories(false),
 2527        vec![
 2528            PathBuf::from(path!("/")),
 2529            PathBuf::from(path!("/root/")),
 2530            PathBuf::from(path!("/root/src/"))
 2531        ]
 2532    );
 2533
 2534    panel.update_in(cx, |panel, window, cx| {
 2535        panel.undo(&Undo, window, cx);
 2536    });
 2537    cx.run_until_parked();
 2538
 2539    assert_eq!(fs.files().len(), 0);
 2540    assert_eq!(
 2541        fs.directories(false),
 2542        vec![PathBuf::from(path!("/")), PathBuf::from(path!("/root/"))]
 2543    );
 2544}
 2545
 2546#[gpui::test]
 2547async fn test_paste_external_paths(cx: &mut gpui::TestAppContext) {
 2548    init_test(cx);
 2549    set_auto_open_settings(
 2550        cx,
 2551        ProjectPanelAutoOpenSettings {
 2552            on_drop: Some(false),
 2553            ..Default::default()
 2554        },
 2555    );
 2556
 2557    let fs = FakeFs::new(cx.executor());
 2558    fs.insert_tree(
 2559        path!("/root"),
 2560        json!({
 2561            "subdir": {}
 2562        }),
 2563    )
 2564    .await;
 2565
 2566    fs.insert_tree(
 2567        path!("/external"),
 2568        json!({
 2569            "new_file.rs": "fn main() {}"
 2570        }),
 2571    )
 2572    .await;
 2573
 2574    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2575    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2576    let workspace = window
 2577        .read_with(cx, |mw, _| mw.workspace().clone())
 2578        .unwrap();
 2579    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2580    let panel = workspace.update_in(cx, ProjectPanel::new);
 2581    cx.run_until_parked();
 2582
 2583    cx.write_to_clipboard(ClipboardItem {
 2584        entries: vec![GpuiClipboardEntry::ExternalPaths(ExternalPaths(
 2585            smallvec::smallvec![PathBuf::from(path!("/external/new_file.rs"))],
 2586        ))],
 2587    });
 2588
 2589    select_path(&panel, "root/subdir", cx);
 2590    panel.update_in(cx, |panel, window, cx| {
 2591        panel.paste(&Default::default(), window, cx);
 2592    });
 2593    cx.executor().run_until_parked();
 2594
 2595    assert_eq!(
 2596        visible_entries_as_strings(&panel, 0..50, cx),
 2597        &[
 2598            "v root",
 2599            "    v subdir",
 2600            "          new_file.rs  <== selected",
 2601        ],
 2602    );
 2603}
 2604
 2605#[gpui::test]
 2606async fn test_copy_and_cut_write_to_system_clipboard(cx: &mut gpui::TestAppContext) {
 2607    init_test(cx);
 2608
 2609    let fs = FakeFs::new(cx.executor());
 2610    fs.insert_tree(
 2611        path!("/root"),
 2612        json!({
 2613            "file_a.txt": "",
 2614            "file_b.txt": ""
 2615        }),
 2616    )
 2617    .await;
 2618
 2619    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2620    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2621    let workspace = window
 2622        .read_with(cx, |mw, _| mw.workspace().clone())
 2623        .unwrap();
 2624    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2625    let panel = workspace.update_in(cx, ProjectPanel::new);
 2626    cx.run_until_parked();
 2627
 2628    select_path(&panel, "root/file_a.txt", cx);
 2629    panel.update_in(cx, |panel, window, cx| {
 2630        panel.copy(&Default::default(), window, cx);
 2631    });
 2632
 2633    let clipboard = cx
 2634        .read_from_clipboard()
 2635        .expect("clipboard should have content after copy");
 2636    let text = clipboard.text().expect("clipboard should contain text");
 2637    assert!(
 2638        text.contains("file_a.txt"),
 2639        "System clipboard should contain the copied file path, got: {text}"
 2640    );
 2641
 2642    select_path(&panel, "root/file_b.txt", cx);
 2643    panel.update_in(cx, |panel, window, cx| {
 2644        panel.cut(&Default::default(), window, cx);
 2645    });
 2646
 2647    let clipboard = cx
 2648        .read_from_clipboard()
 2649        .expect("clipboard should have content after cut");
 2650    let text = clipboard.text().expect("clipboard should contain text");
 2651    assert!(
 2652        text.contains("file_b.txt"),
 2653        "System clipboard should contain the cut file path, got: {text}"
 2654    );
 2655}
 2656
 2657#[gpui::test]
 2658async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
 2659    init_test_with_editor(cx);
 2660
 2661    let fs = FakeFs::new(cx.executor());
 2662    fs.insert_tree(
 2663        path!("/src"),
 2664        json!({
 2665            "test": {
 2666                "first.rs": "// First Rust file",
 2667                "second.rs": "// Second Rust file",
 2668                "third.rs": "// Third Rust file",
 2669            }
 2670        }),
 2671    )
 2672    .await;
 2673
 2674    let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
 2675    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2676    let workspace = window
 2677        .read_with(cx, |mw, _| mw.workspace().clone())
 2678        .unwrap();
 2679    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2680    let panel = workspace.update_in(cx, ProjectPanel::new);
 2681    cx.run_until_parked();
 2682
 2683    toggle_expand_dir(&panel, "src/test", cx);
 2684    select_path(&panel, "src/test/first.rs", cx);
 2685    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 2686    cx.executor().run_until_parked();
 2687    assert_eq!(
 2688        visible_entries_as_strings(&panel, 0..10, cx),
 2689        &[
 2690            "v src",
 2691            "    v test",
 2692            "          first.rs  <== selected  <== marked",
 2693            "          second.rs",
 2694            "          third.rs"
 2695        ]
 2696    );
 2697    ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
 2698
 2699    submit_deletion(&panel, cx);
 2700    assert_eq!(
 2701        visible_entries_as_strings(&panel, 0..10, cx),
 2702        &[
 2703            "v src",
 2704            "    v test",
 2705            "          second.rs  <== selected",
 2706            "          third.rs"
 2707        ],
 2708        "Project panel should have no deleted file, no other file is selected in it"
 2709    );
 2710    ensure_no_open_items_and_panes(&workspace, cx);
 2711
 2712    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 2713    cx.executor().run_until_parked();
 2714    assert_eq!(
 2715        visible_entries_as_strings(&panel, 0..10, cx),
 2716        &[
 2717            "v src",
 2718            "    v test",
 2719            "          second.rs  <== selected  <== marked",
 2720            "          third.rs"
 2721        ]
 2722    );
 2723    ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
 2724
 2725    workspace.update_in(cx, |workspace, window, cx| {
 2726        let active_items = workspace
 2727            .panes()
 2728            .iter()
 2729            .filter_map(|pane| pane.read(cx).active_item())
 2730            .collect::<Vec<_>>();
 2731        assert_eq!(active_items.len(), 1);
 2732        let open_editor = active_items
 2733            .into_iter()
 2734            .next()
 2735            .unwrap()
 2736            .downcast::<Editor>()
 2737            .expect("Open item should be an editor");
 2738        open_editor.update(cx, |editor, cx| {
 2739            editor.set_text("Another text!", window, cx)
 2740        });
 2741    });
 2742    submit_deletion_skipping_prompt(&panel, cx);
 2743    assert_eq!(
 2744        visible_entries_as_strings(&panel, 0..10, cx),
 2745        &["v src", "    v test", "          third.rs  <== selected"],
 2746        "Project panel should have no deleted file, with one last file remaining"
 2747    );
 2748    ensure_no_open_items_and_panes(&workspace, cx);
 2749}
 2750
 2751#[gpui::test]
 2752async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) {
 2753    init_test_with_editor(cx);
 2754    set_auto_open_settings(
 2755        cx,
 2756        ProjectPanelAutoOpenSettings {
 2757            on_create: Some(true),
 2758            ..Default::default()
 2759        },
 2760    );
 2761
 2762    let fs = FakeFs::new(cx.executor());
 2763    fs.insert_tree(path!("/root"), json!({})).await;
 2764
 2765    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2766    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2767    let workspace = window
 2768        .read_with(cx, |mw, _| mw.workspace().clone())
 2769        .unwrap();
 2770    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2771    let panel = workspace.update_in(cx, ProjectPanel::new);
 2772    cx.run_until_parked();
 2773
 2774    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 2775    cx.run_until_parked();
 2776    panel
 2777        .update_in(cx, |panel, window, cx| {
 2778            panel.filename_editor.update(cx, |editor, cx| {
 2779                editor.set_text("auto-open.rs", window, cx);
 2780            });
 2781            panel.confirm_edit(true, window, cx).unwrap()
 2782        })
 2783        .await
 2784        .unwrap();
 2785    cx.run_until_parked();
 2786
 2787    ensure_single_file_is_opened(&workspace, "auto-open.rs", cx);
 2788}
 2789
 2790#[gpui::test]
 2791async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) {
 2792    init_test_with_editor(cx);
 2793    set_auto_open_settings(
 2794        cx,
 2795        ProjectPanelAutoOpenSettings {
 2796            on_create: Some(false),
 2797            ..Default::default()
 2798        },
 2799    );
 2800
 2801    let fs = FakeFs::new(cx.executor());
 2802    fs.insert_tree(path!("/root"), json!({})).await;
 2803
 2804    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2805    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2806    let workspace = window
 2807        .read_with(cx, |mw, _| mw.workspace().clone())
 2808        .unwrap();
 2809    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2810    let panel = workspace.update_in(cx, ProjectPanel::new);
 2811    cx.run_until_parked();
 2812
 2813    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 2814    cx.run_until_parked();
 2815    panel
 2816        .update_in(cx, |panel, window, cx| {
 2817            panel.filename_editor.update(cx, |editor, cx| {
 2818                editor.set_text("manual-open.rs", window, cx);
 2819            });
 2820            panel.confirm_edit(true, window, cx).unwrap()
 2821        })
 2822        .await
 2823        .unwrap();
 2824    cx.run_until_parked();
 2825
 2826    ensure_no_open_items_and_panes(&workspace, cx);
 2827}
 2828
 2829#[gpui::test]
 2830async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) {
 2831    init_test_with_editor(cx);
 2832    set_auto_open_settings(
 2833        cx,
 2834        ProjectPanelAutoOpenSettings {
 2835            on_paste: Some(true),
 2836            ..Default::default()
 2837        },
 2838    );
 2839
 2840    let fs = FakeFs::new(cx.executor());
 2841    fs.insert_tree(
 2842        path!("/root"),
 2843        json!({
 2844            "src": {
 2845                "original.rs": ""
 2846            },
 2847            "target": {}
 2848        }),
 2849    )
 2850    .await;
 2851
 2852    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2853    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2854    let workspace = window
 2855        .read_with(cx, |mw, _| mw.workspace().clone())
 2856        .unwrap();
 2857    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2858    let panel = workspace.update_in(cx, ProjectPanel::new);
 2859    cx.run_until_parked();
 2860
 2861    toggle_expand_dir(&panel, "root/src", cx);
 2862    toggle_expand_dir(&panel, "root/target", cx);
 2863
 2864    select_path(&panel, "root/src/original.rs", cx);
 2865    panel.update_in(cx, |panel, window, cx| {
 2866        panel.copy(&Default::default(), window, cx);
 2867    });
 2868
 2869    select_path(&panel, "root/target", cx);
 2870    panel.update_in(cx, |panel, window, cx| {
 2871        panel.paste(&Default::default(), window, cx);
 2872    });
 2873    cx.executor().run_until_parked();
 2874
 2875    ensure_single_file_is_opened(&workspace, "target/original.rs", cx);
 2876}
 2877
 2878#[gpui::test]
 2879async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) {
 2880    init_test_with_editor(cx);
 2881    set_auto_open_settings(
 2882        cx,
 2883        ProjectPanelAutoOpenSettings {
 2884            on_paste: Some(false),
 2885            ..Default::default()
 2886        },
 2887    );
 2888
 2889    let fs = FakeFs::new(cx.executor());
 2890    fs.insert_tree(
 2891        path!("/root"),
 2892        json!({
 2893            "src": {
 2894                "original.rs": ""
 2895            },
 2896            "target": {}
 2897        }),
 2898    )
 2899    .await;
 2900
 2901    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2902    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2903    let workspace = window
 2904        .read_with(cx, |mw, _| mw.workspace().clone())
 2905        .unwrap();
 2906    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2907    let panel = workspace.update_in(cx, ProjectPanel::new);
 2908    cx.run_until_parked();
 2909
 2910    toggle_expand_dir(&panel, "root/src", cx);
 2911    toggle_expand_dir(&panel, "root/target", cx);
 2912
 2913    select_path(&panel, "root/src/original.rs", cx);
 2914    panel.update_in(cx, |panel, window, cx| {
 2915        panel.copy(&Default::default(), window, cx);
 2916    });
 2917
 2918    select_path(&panel, "root/target", cx);
 2919    panel.update_in(cx, |panel, window, cx| {
 2920        panel.paste(&Default::default(), window, cx);
 2921    });
 2922    cx.executor().run_until_parked();
 2923
 2924    ensure_no_open_items_and_panes(&workspace, cx);
 2925    assert!(
 2926        find_project_entry(&panel, "root/target/original.rs", cx).is_some(),
 2927        "Pasted entry should exist even when auto-open is disabled"
 2928    );
 2929}
 2930
 2931#[gpui::test]
 2932async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) {
 2933    init_test_with_editor(cx);
 2934    set_auto_open_settings(
 2935        cx,
 2936        ProjectPanelAutoOpenSettings {
 2937            on_drop: Some(true),
 2938            ..Default::default()
 2939        },
 2940    );
 2941
 2942    let fs = FakeFs::new(cx.executor());
 2943    fs.insert_tree(path!("/root"), json!({})).await;
 2944
 2945    let temp_dir = tempfile::tempdir().unwrap();
 2946    let external_path = temp_dir.path().join("dropped.rs");
 2947    std::fs::write(&external_path, "// dropped").unwrap();
 2948    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
 2949        .await;
 2950
 2951    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2952    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2953    let workspace = window
 2954        .read_with(cx, |mw, _| mw.workspace().clone())
 2955        .unwrap();
 2956    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2957    let panel = workspace.update_in(cx, ProjectPanel::new);
 2958    cx.run_until_parked();
 2959
 2960    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
 2961    panel.update_in(cx, |panel, window, cx| {
 2962        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
 2963    });
 2964    cx.executor().run_until_parked();
 2965
 2966    ensure_single_file_is_opened(&workspace, "dropped.rs", cx);
 2967}
 2968
 2969#[gpui::test]
 2970async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) {
 2971    init_test_with_editor(cx);
 2972    set_auto_open_settings(
 2973        cx,
 2974        ProjectPanelAutoOpenSettings {
 2975            on_drop: Some(false),
 2976            ..Default::default()
 2977        },
 2978    );
 2979
 2980    let fs = FakeFs::new(cx.executor());
 2981    fs.insert_tree(path!("/root"), json!({})).await;
 2982
 2983    let temp_dir = tempfile::tempdir().unwrap();
 2984    let external_path = temp_dir.path().join("manual.rs");
 2985    std::fs::write(&external_path, "// dropped").unwrap();
 2986    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
 2987        .await;
 2988
 2989    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 2990    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 2991    let workspace = window
 2992        .read_with(cx, |mw, _| mw.workspace().clone())
 2993        .unwrap();
 2994    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 2995    let panel = workspace.update_in(cx, ProjectPanel::new);
 2996    cx.run_until_parked();
 2997
 2998    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
 2999    panel.update_in(cx, |panel, window, cx| {
 3000        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
 3001    });
 3002    cx.executor().run_until_parked();
 3003
 3004    ensure_no_open_items_and_panes(&workspace, cx);
 3005    assert!(
 3006        find_project_entry(&panel, "root/manual.rs", cx).is_some(),
 3007        "Dropped entry should exist even when auto-open is disabled"
 3008    );
 3009}
 3010
 3011#[gpui::test]
 3012async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
 3013    init_test_with_editor(cx);
 3014
 3015    let fs = FakeFs::new(cx.executor());
 3016    fs.insert_tree(
 3017        "/src",
 3018        json!({
 3019            "test": {
 3020                "first.rs": "// First Rust file",
 3021                "second.rs": "// Second Rust file",
 3022                "third.rs": "// Third Rust file",
 3023            }
 3024        }),
 3025    )
 3026    .await;
 3027
 3028    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
 3029    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 3030    let workspace = window
 3031        .read_with(cx, |mw, _| mw.workspace().clone())
 3032        .unwrap();
 3033    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 3034    let panel = workspace.update_in(cx, |workspace, window, cx| {
 3035        let panel = ProjectPanel::new(workspace, window, cx);
 3036        workspace.add_panel(panel.clone(), window, cx);
 3037        panel
 3038    });
 3039    cx.run_until_parked();
 3040
 3041    select_path(&panel, "src", cx);
 3042    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3043    cx.executor().run_until_parked();
 3044    assert_eq!(
 3045        visible_entries_as_strings(&panel, 0..10, cx),
 3046        &[
 3047            //
 3048            "v src  <== selected",
 3049            "    > test"
 3050        ]
 3051    );
 3052    panel.update_in(cx, |panel, window, cx| {
 3053        panel.new_directory(&NewDirectory, window, cx)
 3054    });
 3055    cx.run_until_parked();
 3056    panel.update_in(cx, |panel, window, cx| {
 3057        assert!(panel.filename_editor.read(cx).is_focused(window));
 3058    });
 3059    cx.executor().run_until_parked();
 3060    assert_eq!(
 3061        visible_entries_as_strings(&panel, 0..10, cx),
 3062        &[
 3063            //
 3064            "v src",
 3065            "    > [EDITOR: '']  <== selected",
 3066            "    > test"
 3067        ]
 3068    );
 3069    panel.update_in(cx, |panel, window, cx| {
 3070        panel
 3071            .filename_editor
 3072            .update(cx, |editor, cx| editor.set_text("test", window, cx));
 3073        assert!(
 3074            panel.confirm_edit(true, window, cx).is_none(),
 3075            "Should not allow to confirm on conflicting new directory name"
 3076        );
 3077    });
 3078    cx.executor().run_until_parked();
 3079    panel.update_in(cx, |panel, window, cx| {
 3080        assert!(
 3081            panel.state.edit_state.is_some(),
 3082            "Edit state should not be None after conflicting new directory name"
 3083        );
 3084        panel.cancel(&menu::Cancel, window, cx);
 3085    });
 3086    cx.run_until_parked();
 3087    assert_eq!(
 3088        visible_entries_as_strings(&panel, 0..10, cx),
 3089        &[
 3090            //
 3091            "v src  <== selected",
 3092            "    > test"
 3093        ],
 3094        "File list should be unchanged after failed folder create confirmation"
 3095    );
 3096
 3097    select_path(&panel, "src/test", cx);
 3098    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3099    cx.executor().run_until_parked();
 3100    assert_eq!(
 3101        visible_entries_as_strings(&panel, 0..10, cx),
 3102        &[
 3103            //
 3104            "v src",
 3105            "    > test  <== selected"
 3106        ]
 3107    );
 3108    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 3109    cx.run_until_parked();
 3110    panel.update_in(cx, |panel, window, cx| {
 3111        assert!(panel.filename_editor.read(cx).is_focused(window));
 3112    });
 3113    assert_eq!(
 3114        visible_entries_as_strings(&panel, 0..10, cx),
 3115        &[
 3116            "v src",
 3117            "    v test",
 3118            "          [EDITOR: '']  <== selected",
 3119            "          first.rs",
 3120            "          second.rs",
 3121            "          third.rs"
 3122        ]
 3123    );
 3124    panel.update_in(cx, |panel, window, cx| {
 3125        panel
 3126            .filename_editor
 3127            .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
 3128        assert!(
 3129            panel.confirm_edit(true, window, cx).is_none(),
 3130            "Should not allow to confirm on conflicting new file name"
 3131        );
 3132    });
 3133    cx.executor().run_until_parked();
 3134    panel.update_in(cx, |panel, window, cx| {
 3135        assert!(
 3136            panel.state.edit_state.is_some(),
 3137            "Edit state should not be None after conflicting new file name"
 3138        );
 3139        panel.cancel(&menu::Cancel, window, cx);
 3140    });
 3141    cx.run_until_parked();
 3142    assert_eq!(
 3143        visible_entries_as_strings(&panel, 0..10, cx),
 3144        &[
 3145            "v src",
 3146            "    v test  <== selected",
 3147            "          first.rs",
 3148            "          second.rs",
 3149            "          third.rs"
 3150        ],
 3151        "File list should be unchanged after failed file create confirmation"
 3152    );
 3153
 3154    select_path(&panel, "src/test/first.rs", cx);
 3155    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3156    cx.executor().run_until_parked();
 3157    assert_eq!(
 3158        visible_entries_as_strings(&panel, 0..10, cx),
 3159        &[
 3160            "v src",
 3161            "    v test",
 3162            "          first.rs  <== selected",
 3163            "          second.rs",
 3164            "          third.rs"
 3165        ],
 3166    );
 3167    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 3168    cx.executor().run_until_parked();
 3169    panel.update_in(cx, |panel, window, cx| {
 3170        assert!(panel.filename_editor.read(cx).is_focused(window));
 3171    });
 3172    assert_eq!(
 3173        visible_entries_as_strings(&panel, 0..10, cx),
 3174        &[
 3175            "v src",
 3176            "    v test",
 3177            "          [EDITOR: 'first.rs']  <== selected",
 3178            "          second.rs",
 3179            "          third.rs"
 3180        ]
 3181    );
 3182    panel.update_in(cx, |panel, window, cx| {
 3183        panel
 3184            .filename_editor
 3185            .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
 3186        assert!(
 3187            panel.confirm_edit(true, window, cx).is_none(),
 3188            "Should not allow to confirm on conflicting file rename"
 3189        )
 3190    });
 3191    cx.executor().run_until_parked();
 3192    panel.update_in(cx, |panel, window, cx| {
 3193        assert!(
 3194            panel.state.edit_state.is_some(),
 3195            "Edit state should not be None after conflicting file rename"
 3196        );
 3197        panel.cancel(&menu::Cancel, window, cx);
 3198    });
 3199    cx.executor().run_until_parked();
 3200    assert_eq!(
 3201        visible_entries_as_strings(&panel, 0..10, cx),
 3202        &[
 3203            "v src",
 3204            "    v test",
 3205            "          first.rs  <== selected",
 3206            "          second.rs",
 3207            "          third.rs"
 3208        ],
 3209        "File list should be unchanged after failed rename confirmation"
 3210    );
 3211}
 3212
 3213// NOTE: This test is skipped on Windows, because on Windows,
 3214// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
 3215// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
 3216#[gpui::test]
 3217#[cfg_attr(target_os = "windows", ignore)]
 3218async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
 3219    init_test_with_editor(cx);
 3220
 3221    let fs = FakeFs::new(cx.executor());
 3222    fs.insert_tree(
 3223        "/src",
 3224        json!({
 3225            "test": {
 3226                "first.txt": "// First Txt file",
 3227                "second.txt": "// Second Txt file",
 3228                "third.txt": "// Third Txt file",
 3229            }
 3230        }),
 3231    )
 3232    .await;
 3233
 3234    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
 3235    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 3236    let workspace = window
 3237        .read_with(cx, |mw, _| mw.workspace().clone())
 3238        .unwrap();
 3239    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 3240    let panel = workspace.update_in(cx, |workspace, window, cx| {
 3241        let panel = ProjectPanel::new(workspace, window, cx);
 3242        workspace.add_panel(panel.clone(), window, cx);
 3243        panel
 3244    });
 3245    cx.run_until_parked();
 3246
 3247    select_path(&panel, "src", cx);
 3248    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3249    cx.executor().run_until_parked();
 3250    assert_eq!(
 3251        visible_entries_as_strings(&panel, 0..10, cx),
 3252        &[
 3253            //
 3254            "v src  <== selected",
 3255            "    > test"
 3256        ]
 3257    );
 3258    panel.update_in(cx, |panel, window, cx| {
 3259        panel.new_directory(&NewDirectory, window, cx)
 3260    });
 3261    cx.run_until_parked();
 3262    panel.update_in(cx, |panel, window, cx| {
 3263        assert!(panel.filename_editor.read(cx).is_focused(window));
 3264    });
 3265    cx.executor().run_until_parked();
 3266    assert_eq!(
 3267        visible_entries_as_strings(&panel, 0..10, cx),
 3268        &[
 3269            //
 3270            "v src",
 3271            "    > [EDITOR: '']  <== selected",
 3272            "    > test"
 3273        ]
 3274    );
 3275    panel.update_in(cx, |panel, window, cx| {
 3276        panel
 3277            .filename_editor
 3278            .update(cx, |editor, cx| editor.set_text("test", window, cx));
 3279        assert!(
 3280            panel.confirm_edit(true, window, cx).is_none(),
 3281            "Should not allow to confirm on conflicting new directory name"
 3282        );
 3283    });
 3284    cx.executor().run_until_parked();
 3285    panel.update_in(cx, |panel, window, cx| {
 3286        assert!(
 3287            panel.state.edit_state.is_some(),
 3288            "Edit state should not be None after conflicting new directory name"
 3289        );
 3290        panel.cancel(&menu::Cancel, window, cx);
 3291    });
 3292    cx.run_until_parked();
 3293    assert_eq!(
 3294        visible_entries_as_strings(&panel, 0..10, cx),
 3295        &[
 3296            //
 3297            "v src  <== selected",
 3298            "    > test"
 3299        ],
 3300        "File list should be unchanged after failed folder create confirmation"
 3301    );
 3302
 3303    select_path(&panel, "src/test", cx);
 3304    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3305    cx.executor().run_until_parked();
 3306    assert_eq!(
 3307        visible_entries_as_strings(&panel, 0..10, cx),
 3308        &[
 3309            //
 3310            "v src",
 3311            "    > test  <== selected"
 3312        ]
 3313    );
 3314    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 3315    cx.run_until_parked();
 3316    panel.update_in(cx, |panel, window, cx| {
 3317        assert!(panel.filename_editor.read(cx).is_focused(window));
 3318    });
 3319    assert_eq!(
 3320        visible_entries_as_strings(&panel, 0..10, cx),
 3321        &[
 3322            "v src",
 3323            "    v test",
 3324            "          [EDITOR: '']  <== selected",
 3325            "          first.txt",
 3326            "          second.txt",
 3327            "          third.txt"
 3328        ]
 3329    );
 3330    panel.update_in(cx, |panel, window, cx| {
 3331        panel
 3332            .filename_editor
 3333            .update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
 3334        assert!(
 3335            panel.confirm_edit(true, window, cx).is_none(),
 3336            "Should not allow to confirm on conflicting new file name"
 3337        );
 3338    });
 3339    cx.executor().run_until_parked();
 3340    panel.update_in(cx, |panel, window, cx| {
 3341        assert!(
 3342            panel.state.edit_state.is_some(),
 3343            "Edit state should not be None after conflicting new file name"
 3344        );
 3345        panel.cancel(&menu::Cancel, window, cx);
 3346    });
 3347    cx.run_until_parked();
 3348    assert_eq!(
 3349        visible_entries_as_strings(&panel, 0..10, cx),
 3350        &[
 3351            "v src",
 3352            "    v test  <== selected",
 3353            "          first.txt",
 3354            "          second.txt",
 3355            "          third.txt"
 3356        ],
 3357        "File list should be unchanged after failed file create confirmation"
 3358    );
 3359
 3360    select_path(&panel, "src/test/first.txt", cx);
 3361    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3362    cx.executor().run_until_parked();
 3363    assert_eq!(
 3364        visible_entries_as_strings(&panel, 0..10, cx),
 3365        &[
 3366            "v src",
 3367            "    v test",
 3368            "          first.txt  <== selected",
 3369            "          second.txt",
 3370            "          third.txt"
 3371        ],
 3372    );
 3373    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 3374    cx.executor().run_until_parked();
 3375    panel.update_in(cx, |panel, window, cx| {
 3376        assert!(panel.filename_editor.read(cx).is_focused(window));
 3377    });
 3378    assert_eq!(
 3379        visible_entries_as_strings(&panel, 0..10, cx),
 3380        &[
 3381            "v src",
 3382            "    v test",
 3383            "          [EDITOR: 'first.txt']  <== selected",
 3384            "          second.txt",
 3385            "          third.txt"
 3386        ]
 3387    );
 3388    panel.update_in(cx, |panel, window, cx| {
 3389        panel
 3390            .filename_editor
 3391            .update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
 3392        assert!(
 3393            panel.confirm_edit(true, window, cx).is_none(),
 3394            "Should not allow to confirm on conflicting file rename"
 3395        )
 3396    });
 3397    cx.executor().run_until_parked();
 3398    panel.update_in(cx, |panel, window, cx| {
 3399        assert!(
 3400            panel.state.edit_state.is_some(),
 3401            "Edit state should not be None after conflicting file rename"
 3402        );
 3403        panel.cancel(&menu::Cancel, window, cx);
 3404    });
 3405    cx.executor().run_until_parked();
 3406    assert_eq!(
 3407        visible_entries_as_strings(&panel, 0..10, cx),
 3408        &[
 3409            "v src",
 3410            "    v test",
 3411            "          first.txt  <== selected",
 3412            "          second.txt",
 3413            "          third.txt"
 3414        ],
 3415        "File list should be unchanged after failed rename confirmation"
 3416    );
 3417    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 3418    cx.executor().run_until_parked();
 3419    // Try to duplicate and check history
 3420    panel.update_in(cx, |panel, window, cx| {
 3421        panel.duplicate(&Duplicate, window, cx)
 3422    });
 3423    cx.executor().run_until_parked();
 3424
 3425    assert_eq!(
 3426        visible_entries_as_strings(&panel, 0..10, cx),
 3427        &[
 3428            "v src",
 3429            "    v test",
 3430            "          first.txt",
 3431            "          [EDITOR: 'first copy.txt']  <== selected  <== marked",
 3432            "          second.txt",
 3433            "          third.txt"
 3434        ],
 3435    );
 3436
 3437    let confirm = panel.update_in(cx, |panel, window, cx| {
 3438        panel
 3439            .filename_editor
 3440            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
 3441        panel.confirm_edit(true, window, cx).unwrap()
 3442    });
 3443    confirm.await.unwrap();
 3444    cx.executor().run_until_parked();
 3445
 3446    assert_eq!(
 3447        visible_entries_as_strings(&panel, 0..10, cx),
 3448        &[
 3449            "v src",
 3450            "    v test",
 3451            "          first.txt",
 3452            "          fourth.txt  <== selected",
 3453            "          second.txt",
 3454            "          third.txt"
 3455        ],
 3456        "File list should be different after rename confirmation"
 3457    );
 3458
 3459    panel.update_in(cx, |panel, window, cx| {
 3460        panel.update_visible_entries(None, false, false, window, cx);
 3461    });
 3462    cx.executor().run_until_parked();
 3463
 3464    select_path(&panel, "src/test/first.txt", cx);
 3465    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 3466    cx.executor().run_until_parked();
 3467
 3468    workspace.read_with(cx, |this, cx| {
 3469        assert!(
 3470            this.recent_navigation_history_iter(cx)
 3471                .any(|(project_path, abs_path)| {
 3472                    project_path.path == Arc::from(rel_path("test/fourth.txt"))
 3473                        && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
 3474                })
 3475        );
 3476    });
 3477}
 3478
 3479// NOTE: This test is skipped on Windows, because on Windows,
 3480// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
 3481// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
 3482#[gpui::test]
 3483#[cfg_attr(target_os = "windows", ignore)]
 3484async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
 3485    init_test_with_editor(cx);
 3486
 3487    let fs = FakeFs::new(cx.executor());
 3488    fs.insert_tree(
 3489        "/src",
 3490        json!({
 3491            "test": {
 3492                "first.txt": "// First Txt file",
 3493                "second.txt": "// Second Txt file",
 3494                "third.txt": "// Third Txt file",
 3495            }
 3496        }),
 3497    )
 3498    .await;
 3499
 3500    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
 3501    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 3502    let workspace = window
 3503        .read_with(cx, |mw, _| mw.workspace().clone())
 3504        .unwrap();
 3505    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 3506    let panel = workspace.update_in(cx, |workspace, window, cx| {
 3507        let panel = ProjectPanel::new(workspace, window, cx);
 3508        workspace.add_panel(panel.clone(), window, cx);
 3509        panel
 3510    });
 3511    cx.run_until_parked();
 3512
 3513    select_path(&panel, "src", cx);
 3514    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3515    cx.executor().run_until_parked();
 3516    assert_eq!(
 3517        visible_entries_as_strings(&panel, 0..10, cx),
 3518        &[
 3519            //
 3520            "v src  <== selected",
 3521            "    > test"
 3522        ]
 3523    );
 3524
 3525    select_path(&panel, "src/test", cx);
 3526    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 3527    cx.executor().run_until_parked();
 3528    assert_eq!(
 3529        visible_entries_as_strings(&panel, 0..10, cx),
 3530        &[
 3531            //
 3532            "v src",
 3533            "    > test  <== selected"
 3534        ]
 3535    );
 3536    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 3537    cx.run_until_parked();
 3538    panel.update_in(cx, |panel, window, cx| {
 3539        assert!(panel.filename_editor.read(cx).is_focused(window));
 3540    });
 3541
 3542    select_path(&panel, "src/test/first.txt", cx);
 3543    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 3544    cx.executor().run_until_parked();
 3545
 3546    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 3547    cx.executor().run_until_parked();
 3548
 3549    assert_eq!(
 3550        visible_entries_as_strings(&panel, 0..10, cx),
 3551        &[
 3552            "v src",
 3553            "    v test",
 3554            "          [EDITOR: 'first.txt']  <== selected  <== marked",
 3555            "          second.txt",
 3556            "          third.txt"
 3557        ],
 3558    );
 3559
 3560    let confirm = panel.update_in(cx, |panel, window, cx| {
 3561        panel
 3562            .filename_editor
 3563            .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
 3564        panel.confirm_edit(true, window, cx).unwrap()
 3565    });
 3566    confirm.await.unwrap();
 3567    cx.executor().run_until_parked();
 3568
 3569    assert_eq!(
 3570        visible_entries_as_strings(&panel, 0..10, cx),
 3571        &[
 3572            "v src",
 3573            "    v test",
 3574            "          fourth.txt  <== selected",
 3575            "          second.txt",
 3576            "          third.txt"
 3577        ],
 3578        "File list should be different after rename confirmation"
 3579    );
 3580
 3581    panel.update_in(cx, |panel, window, cx| {
 3582        panel.update_visible_entries(None, false, false, window, cx);
 3583    });
 3584    cx.executor().run_until_parked();
 3585
 3586    select_path(&panel, "src/test/second.txt", cx);
 3587    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 3588    cx.executor().run_until_parked();
 3589
 3590    workspace.read_with(cx, |this, cx| {
 3591        assert!(
 3592            this.recent_navigation_history_iter(cx)
 3593                .any(|(project_path, abs_path)| {
 3594                    project_path.path == Arc::from(rel_path("test/fourth.txt"))
 3595                        && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
 3596                })
 3597        );
 3598    });
 3599}
 3600
 3601#[gpui::test]
 3602async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
 3603    init_test_with_editor(cx);
 3604
 3605    let fs = FakeFs::new(cx.executor());
 3606    fs.insert_tree(
 3607        path!("/root"),
 3608        json!({
 3609            "tree1": {
 3610                ".git": {},
 3611                "dir1": {
 3612                    "modified1.txt": "1",
 3613                    "unmodified1.txt": "1",
 3614                    "modified2.txt": "1",
 3615                },
 3616                "dir2": {
 3617                    "modified3.txt": "1",
 3618                    "unmodified2.txt": "1",
 3619                },
 3620                "modified4.txt": "1",
 3621                "unmodified3.txt": "1",
 3622            },
 3623            "tree2": {
 3624                ".git": {},
 3625                "dir3": {
 3626                    "modified5.txt": "1",
 3627                    "unmodified4.txt": "1",
 3628                },
 3629                "modified6.txt": "1",
 3630                "unmodified5.txt": "1",
 3631            }
 3632        }),
 3633    )
 3634    .await;
 3635
 3636    // Mark files as git modified
 3637    fs.set_head_and_index_for_repo(
 3638        path!("/root/tree1/.git").as_ref(),
 3639        &[
 3640            ("dir1/modified1.txt", "modified".into()),
 3641            ("dir1/modified2.txt", "modified".into()),
 3642            ("modified4.txt", "modified".into()),
 3643            ("dir2/modified3.txt", "modified".into()),
 3644        ],
 3645    );
 3646    fs.set_head_and_index_for_repo(
 3647        path!("/root/tree2/.git").as_ref(),
 3648        &[
 3649            ("dir3/modified5.txt", "modified".into()),
 3650            ("modified6.txt", "modified".into()),
 3651        ],
 3652    );
 3653
 3654    let project = Project::test(
 3655        fs.clone(),
 3656        [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
 3657        cx,
 3658    )
 3659    .await;
 3660
 3661    let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
 3662        let mut worktrees = project.worktrees(cx);
 3663        let worktree1 = worktrees.next().unwrap();
 3664        let worktree2 = worktrees.next().unwrap();
 3665        (
 3666            worktree1.read(cx).as_local().unwrap().scan_complete(),
 3667            worktree2.read(cx).as_local().unwrap().scan_complete(),
 3668        )
 3669    });
 3670    scan1_complete.await;
 3671    scan2_complete.await;
 3672    cx.run_until_parked();
 3673
 3674    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 3675    let workspace = window
 3676        .read_with(cx, |mw, _| mw.workspace().clone())
 3677        .unwrap();
 3678    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 3679    let panel = workspace.update_in(cx, ProjectPanel::new);
 3680    cx.run_until_parked();
 3681
 3682    // Check initial state
 3683    assert_eq!(
 3684        visible_entries_as_strings(&panel, 0..15, cx),
 3685        &[
 3686            "v tree1",
 3687            "    > .git",
 3688            "    > dir1",
 3689            "    > dir2",
 3690            "      modified4.txt",
 3691            "      unmodified3.txt",
 3692            "v tree2",
 3693            "    > .git",
 3694            "    > dir3",
 3695            "      modified6.txt",
 3696            "      unmodified5.txt"
 3697        ],
 3698    );
 3699
 3700    // Test selecting next modified entry
 3701    panel.update_in(cx, |panel, window, cx| {
 3702        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3703    });
 3704    cx.run_until_parked();
 3705
 3706    assert_eq!(
 3707        visible_entries_as_strings(&panel, 0..6, cx),
 3708        &[
 3709            "v tree1",
 3710            "    > .git",
 3711            "    v dir1",
 3712            "          modified1.txt  <== selected",
 3713            "          modified2.txt",
 3714            "          unmodified1.txt",
 3715        ],
 3716    );
 3717
 3718    panel.update_in(cx, |panel, window, cx| {
 3719        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3720    });
 3721    cx.run_until_parked();
 3722
 3723    assert_eq!(
 3724        visible_entries_as_strings(&panel, 0..6, cx),
 3725        &[
 3726            "v tree1",
 3727            "    > .git",
 3728            "    v dir1",
 3729            "          modified1.txt",
 3730            "          modified2.txt  <== selected",
 3731            "          unmodified1.txt",
 3732        ],
 3733    );
 3734
 3735    panel.update_in(cx, |panel, window, cx| {
 3736        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3737    });
 3738    cx.run_until_parked();
 3739
 3740    assert_eq!(
 3741        visible_entries_as_strings(&panel, 6..9, cx),
 3742        &[
 3743            "    v dir2",
 3744            "          modified3.txt  <== selected",
 3745            "          unmodified2.txt",
 3746        ],
 3747    );
 3748
 3749    panel.update_in(cx, |panel, window, cx| {
 3750        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3751    });
 3752    cx.run_until_parked();
 3753
 3754    assert_eq!(
 3755        visible_entries_as_strings(&panel, 9..11, cx),
 3756        &["      modified4.txt  <== selected", "      unmodified3.txt",],
 3757    );
 3758
 3759    panel.update_in(cx, |panel, window, cx| {
 3760        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3761    });
 3762    cx.run_until_parked();
 3763
 3764    assert_eq!(
 3765        visible_entries_as_strings(&panel, 13..16, cx),
 3766        &[
 3767            "    v dir3",
 3768            "          modified5.txt  <== selected",
 3769            "          unmodified4.txt",
 3770        ],
 3771    );
 3772
 3773    panel.update_in(cx, |panel, window, cx| {
 3774        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3775    });
 3776    cx.run_until_parked();
 3777
 3778    assert_eq!(
 3779        visible_entries_as_strings(&panel, 16..18, cx),
 3780        &["      modified6.txt  <== selected", "      unmodified5.txt",],
 3781    );
 3782
 3783    // Wraps around to first modified file
 3784    panel.update_in(cx, |panel, window, cx| {
 3785        panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
 3786    });
 3787    cx.run_until_parked();
 3788
 3789    assert_eq!(
 3790        visible_entries_as_strings(&panel, 0..18, cx),
 3791        &[
 3792            "v tree1",
 3793            "    > .git",
 3794            "    v dir1",
 3795            "          modified1.txt  <== selected",
 3796            "          modified2.txt",
 3797            "          unmodified1.txt",
 3798            "    v dir2",
 3799            "          modified3.txt",
 3800            "          unmodified2.txt",
 3801            "      modified4.txt",
 3802            "      unmodified3.txt",
 3803            "v tree2",
 3804            "    > .git",
 3805            "    v dir3",
 3806            "          modified5.txt",
 3807            "          unmodified4.txt",
 3808            "      modified6.txt",
 3809            "      unmodified5.txt",
 3810        ],
 3811    );
 3812
 3813    // Wraps around again to last modified file
 3814    panel.update_in(cx, |panel, window, cx| {
 3815        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3816    });
 3817    cx.run_until_parked();
 3818
 3819    assert_eq!(
 3820        visible_entries_as_strings(&panel, 16..18, cx),
 3821        &["      modified6.txt  <== selected", "      unmodified5.txt",],
 3822    );
 3823
 3824    panel.update_in(cx, |panel, window, cx| {
 3825        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3826    });
 3827    cx.run_until_parked();
 3828
 3829    assert_eq!(
 3830        visible_entries_as_strings(&panel, 13..16, cx),
 3831        &[
 3832            "    v dir3",
 3833            "          modified5.txt  <== selected",
 3834            "          unmodified4.txt",
 3835        ],
 3836    );
 3837
 3838    panel.update_in(cx, |panel, window, cx| {
 3839        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3840    });
 3841    cx.run_until_parked();
 3842
 3843    assert_eq!(
 3844        visible_entries_as_strings(&panel, 9..11, cx),
 3845        &["      modified4.txt  <== selected", "      unmodified3.txt",],
 3846    );
 3847
 3848    panel.update_in(cx, |panel, window, cx| {
 3849        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3850    });
 3851    cx.run_until_parked();
 3852
 3853    assert_eq!(
 3854        visible_entries_as_strings(&panel, 6..9, cx),
 3855        &[
 3856            "    v dir2",
 3857            "          modified3.txt  <== selected",
 3858            "          unmodified2.txt",
 3859        ],
 3860    );
 3861
 3862    panel.update_in(cx, |panel, window, cx| {
 3863        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3864    });
 3865    cx.run_until_parked();
 3866
 3867    assert_eq!(
 3868        visible_entries_as_strings(&panel, 0..6, cx),
 3869        &[
 3870            "v tree1",
 3871            "    > .git",
 3872            "    v dir1",
 3873            "          modified1.txt",
 3874            "          modified2.txt  <== selected",
 3875            "          unmodified1.txt",
 3876        ],
 3877    );
 3878
 3879    panel.update_in(cx, |panel, window, cx| {
 3880        panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
 3881    });
 3882    cx.run_until_parked();
 3883
 3884    assert_eq!(
 3885        visible_entries_as_strings(&panel, 0..6, cx),
 3886        &[
 3887            "v tree1",
 3888            "    > .git",
 3889            "    v dir1",
 3890            "          modified1.txt  <== selected",
 3891            "          modified2.txt",
 3892            "          unmodified1.txt",
 3893        ],
 3894    );
 3895}
 3896
 3897#[gpui::test]
 3898async fn test_select_directory(cx: &mut gpui::TestAppContext) {
 3899    init_test_with_editor(cx);
 3900
 3901    let fs = FakeFs::new(cx.executor());
 3902    fs.insert_tree(
 3903        "/project_root",
 3904        json!({
 3905            "dir_1": {
 3906                "nested_dir": {
 3907                    "file_a.py": "# File contents",
 3908                }
 3909            },
 3910            "file_1.py": "# File contents",
 3911            "dir_2": {
 3912
 3913            },
 3914            "dir_3": {
 3915
 3916            },
 3917            "file_2.py": "# File contents",
 3918            "dir_4": {
 3919
 3920            },
 3921        }),
 3922    )
 3923    .await;
 3924
 3925    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 3926    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 3927    let workspace = window
 3928        .read_with(cx, |mw, _| mw.workspace().clone())
 3929        .unwrap();
 3930    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 3931    let panel = workspace.update_in(cx, ProjectPanel::new);
 3932    cx.run_until_parked();
 3933
 3934    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 3935    cx.executor().run_until_parked();
 3936    select_path(&panel, "project_root/dir_1", cx);
 3937    cx.executor().run_until_parked();
 3938    assert_eq!(
 3939        visible_entries_as_strings(&panel, 0..10, cx),
 3940        &[
 3941            "v project_root",
 3942            "    > dir_1  <== selected",
 3943            "    > dir_2",
 3944            "    > dir_3",
 3945            "    > dir_4",
 3946            "      file_1.py",
 3947            "      file_2.py",
 3948        ]
 3949    );
 3950    panel.update_in(cx, |panel, window, cx| {
 3951        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
 3952    });
 3953
 3954    assert_eq!(
 3955        visible_entries_as_strings(&panel, 0..10, cx),
 3956        &[
 3957            "v project_root  <== selected",
 3958            "    > dir_1",
 3959            "    > dir_2",
 3960            "    > dir_3",
 3961            "    > dir_4",
 3962            "      file_1.py",
 3963            "      file_2.py",
 3964        ]
 3965    );
 3966
 3967    panel.update_in(cx, |panel, window, cx| {
 3968        panel.select_prev_directory(&SelectPrevDirectory, window, cx)
 3969    });
 3970
 3971    assert_eq!(
 3972        visible_entries_as_strings(&panel, 0..10, cx),
 3973        &[
 3974            "v project_root",
 3975            "    > dir_1",
 3976            "    > dir_2",
 3977            "    > dir_3",
 3978            "    > dir_4  <== selected",
 3979            "      file_1.py",
 3980            "      file_2.py",
 3981        ]
 3982    );
 3983
 3984    panel.update_in(cx, |panel, window, cx| {
 3985        panel.select_next_directory(&SelectNextDirectory, window, cx)
 3986    });
 3987
 3988    assert_eq!(
 3989        visible_entries_as_strings(&panel, 0..10, cx),
 3990        &[
 3991            "v project_root  <== selected",
 3992            "    > dir_1",
 3993            "    > dir_2",
 3994            "    > dir_3",
 3995            "    > dir_4",
 3996            "      file_1.py",
 3997            "      file_2.py",
 3998        ]
 3999    );
 4000}
 4001
 4002#[gpui::test]
 4003async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
 4004    init_test_with_editor(cx);
 4005
 4006    let fs = FakeFs::new(cx.executor());
 4007    fs.insert_tree(
 4008        "/project_root",
 4009        json!({
 4010            "dir_1": {
 4011                "nested_dir": {
 4012                    "file_a.py": "# File contents",
 4013                }
 4014            },
 4015            "file_1.py": "# File contents",
 4016            "file_2.py": "# File contents",
 4017            "zdir_2": {
 4018                "nested_dir2": {
 4019                    "file_b.py": "# File contents",
 4020                }
 4021            },
 4022        }),
 4023    )
 4024    .await;
 4025
 4026    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4027    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4028    let workspace = window
 4029        .read_with(cx, |mw, _| mw.workspace().clone())
 4030        .unwrap();
 4031    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4032    let panel = workspace.update_in(cx, ProjectPanel::new);
 4033    cx.run_until_parked();
 4034
 4035    assert_eq!(
 4036        visible_entries_as_strings(&panel, 0..10, cx),
 4037        &[
 4038            "v project_root",
 4039            "    > dir_1",
 4040            "    > zdir_2",
 4041            "      file_1.py",
 4042            "      file_2.py",
 4043        ]
 4044    );
 4045    panel.update_in(cx, |panel, window, cx| {
 4046        panel.select_first(&SelectFirst, window, cx)
 4047    });
 4048
 4049    assert_eq!(
 4050        visible_entries_as_strings(&panel, 0..10, cx),
 4051        &[
 4052            "v project_root  <== selected",
 4053            "    > dir_1",
 4054            "    > zdir_2",
 4055            "      file_1.py",
 4056            "      file_2.py",
 4057        ]
 4058    );
 4059
 4060    panel.update_in(cx, |panel, window, cx| {
 4061        panel.select_last(&SelectLast, window, cx)
 4062    });
 4063
 4064    assert_eq!(
 4065        visible_entries_as_strings(&panel, 0..10, cx),
 4066        &[
 4067            "v project_root",
 4068            "    > dir_1",
 4069            "    > zdir_2",
 4070            "      file_1.py",
 4071            "      file_2.py  <== selected",
 4072        ]
 4073    );
 4074
 4075    cx.update(|_, cx| {
 4076        let settings = *ProjectPanelSettings::get_global(cx);
 4077        ProjectPanelSettings::override_global(
 4078            ProjectPanelSettings {
 4079                hide_root: true,
 4080                ..settings
 4081            },
 4082            cx,
 4083        );
 4084    });
 4085
 4086    let panel = workspace.update_in(cx, ProjectPanel::new);
 4087    cx.run_until_parked();
 4088
 4089    #[rustfmt::skip]
 4090    assert_eq!(
 4091        visible_entries_as_strings(&panel, 0..10, cx),
 4092        &[
 4093            "> dir_1",
 4094            "> zdir_2",
 4095            "  file_1.py",
 4096            "  file_2.py",
 4097        ],
 4098        "With hide_root=true, root should be hidden"
 4099    );
 4100
 4101    panel.update_in(cx, |panel, window, cx| {
 4102        panel.select_first(&SelectFirst, window, cx)
 4103    });
 4104
 4105    assert_eq!(
 4106        visible_entries_as_strings(&panel, 0..10, cx),
 4107        &[
 4108            "> dir_1  <== selected",
 4109            "> zdir_2",
 4110            "  file_1.py",
 4111            "  file_2.py",
 4112        ],
 4113        "With hide_root=true, first entry should be dir_1, not the hidden root"
 4114    );
 4115}
 4116
 4117#[gpui::test]
 4118async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
 4119    init_test_with_editor(cx);
 4120
 4121    let fs = FakeFs::new(cx.executor());
 4122    fs.insert_tree(
 4123        "/project_root",
 4124        json!({
 4125            "dir_1": {
 4126                "nested_dir": {
 4127                    "file_a.py": "# File contents",
 4128                }
 4129            },
 4130            "file_1.py": "# File contents",
 4131        }),
 4132    )
 4133    .await;
 4134
 4135    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4136    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4137    let workspace = window
 4138        .read_with(cx, |mw, _| mw.workspace().clone())
 4139        .unwrap();
 4140    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4141    let panel = workspace.update_in(cx, ProjectPanel::new);
 4142    cx.run_until_parked();
 4143
 4144    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 4145    cx.executor().run_until_parked();
 4146    select_path(&panel, "project_root/dir_1", cx);
 4147    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 4148    select_path(&panel, "project_root/dir_1/nested_dir", cx);
 4149    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 4150    panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
 4151    cx.executor().run_until_parked();
 4152    assert_eq!(
 4153        visible_entries_as_strings(&panel, 0..10, cx),
 4154        &[
 4155            "v project_root",
 4156            "    v dir_1",
 4157            "        > nested_dir  <== selected",
 4158            "      file_1.py",
 4159        ]
 4160    );
 4161}
 4162
 4163#[gpui::test]
 4164async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
 4165    init_test_with_editor(cx);
 4166
 4167    let fs = FakeFs::new(cx.executor());
 4168    fs.insert_tree(
 4169        "/project_root",
 4170        json!({
 4171            "dir_1": {
 4172                "nested_dir": {
 4173                    "file_a.py": "# File contents",
 4174                    "file_b.py": "# File contents",
 4175                    "file_c.py": "# File contents",
 4176                },
 4177                "file_1.py": "# File contents",
 4178                "file_2.py": "# File contents",
 4179                "file_3.py": "# File contents",
 4180            },
 4181            "dir_2": {
 4182                "file_1.py": "# File contents",
 4183                "file_2.py": "# File contents",
 4184                "file_3.py": "# File contents",
 4185            }
 4186        }),
 4187    )
 4188    .await;
 4189
 4190    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4191    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4192    let workspace = window
 4193        .read_with(cx, |mw, _| mw.workspace().clone())
 4194        .unwrap();
 4195    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4196    let panel = workspace.update_in(cx, ProjectPanel::new);
 4197    cx.run_until_parked();
 4198
 4199    panel.update_in(cx, |panel, window, cx| {
 4200        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
 4201    });
 4202    cx.executor().run_until_parked();
 4203    assert_eq!(
 4204        visible_entries_as_strings(&panel, 0..10, cx),
 4205        &["v project_root", "    > dir_1", "    > dir_2",]
 4206    );
 4207
 4208    // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
 4209    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 4210    cx.executor().run_until_parked();
 4211    assert_eq!(
 4212        visible_entries_as_strings(&panel, 0..10, cx),
 4213        &[
 4214            "v project_root",
 4215            "    v dir_1  <== selected",
 4216            "        > nested_dir",
 4217            "          file_1.py",
 4218            "          file_2.py",
 4219            "          file_3.py",
 4220            "    > dir_2",
 4221        ]
 4222    );
 4223}
 4224
 4225#[gpui::test]
 4226async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
 4227    init_test_with_editor(cx);
 4228
 4229    let fs = FakeFs::new(cx.executor());
 4230    let worktree_content = json!({
 4231        "dir_1": {
 4232            "file_1.py": "# File contents",
 4233        },
 4234        "dir_2": {
 4235            "file_1.py": "# File contents",
 4236        }
 4237    });
 4238
 4239    fs.insert_tree("/project_root_1", worktree_content.clone())
 4240        .await;
 4241    fs.insert_tree("/project_root_2", worktree_content).await;
 4242
 4243    let project = Project::test(
 4244        fs.clone(),
 4245        ["/project_root_1".as_ref(), "/project_root_2".as_ref()],
 4246        cx,
 4247    )
 4248    .await;
 4249    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4250    let workspace = window
 4251        .read_with(cx, |mw, _| mw.workspace().clone())
 4252        .unwrap();
 4253    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4254    let panel = workspace.update_in(cx, ProjectPanel::new);
 4255    cx.run_until_parked();
 4256
 4257    panel.update_in(cx, |panel, window, cx| {
 4258        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
 4259    });
 4260    cx.executor().run_until_parked();
 4261    assert_eq!(
 4262        visible_entries_as_strings(&panel, 0..10, cx),
 4263        &["> project_root_1", "> project_root_2",]
 4264    );
 4265}
 4266
 4267#[gpui::test]
 4268async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) {
 4269    init_test_with_editor(cx);
 4270
 4271    let fs = FakeFs::new(cx.executor());
 4272    fs.insert_tree(
 4273        "/project_root",
 4274        json!({
 4275            "dir_1": {
 4276                "nested_dir": {
 4277                    "file_a.py": "# File contents",
 4278                    "file_b.py": "# File contents",
 4279                    "file_c.py": "# File contents",
 4280                },
 4281                "file_1.py": "# File contents",
 4282                "file_2.py": "# File contents",
 4283                "file_3.py": "# File contents",
 4284            },
 4285            "dir_2": {
 4286                "file_1.py": "# File contents",
 4287                "file_2.py": "# File contents",
 4288                "file_3.py": "# File contents",
 4289            }
 4290        }),
 4291    )
 4292    .await;
 4293
 4294    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4295    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4296    let workspace = window
 4297        .read_with(cx, |mw, _| mw.workspace().clone())
 4298        .unwrap();
 4299    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4300    let panel = workspace.update_in(cx, ProjectPanel::new);
 4301    cx.run_until_parked();
 4302
 4303    // Open project_root/dir_1 to ensure that a nested directory is expanded
 4304    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 4305    cx.executor().run_until_parked();
 4306    assert_eq!(
 4307        visible_entries_as_strings(&panel, 0..10, cx),
 4308        &[
 4309            "v project_root",
 4310            "    v dir_1  <== selected",
 4311            "        > nested_dir",
 4312            "          file_1.py",
 4313            "          file_2.py",
 4314            "          file_3.py",
 4315            "    > dir_2",
 4316        ]
 4317    );
 4318
 4319    // Close root directory
 4320    toggle_expand_dir(&panel, "project_root", cx);
 4321    cx.executor().run_until_parked();
 4322    assert_eq!(
 4323        visible_entries_as_strings(&panel, 0..10, cx),
 4324        &["> project_root  <== selected"]
 4325    );
 4326
 4327    // Run collapse_all_entries and make sure root is not expanded
 4328    panel.update_in(cx, |panel, window, cx| {
 4329        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
 4330    });
 4331    cx.executor().run_until_parked();
 4332    assert_eq!(
 4333        visible_entries_as_strings(&panel, 0..10, cx),
 4334        &["> project_root  <== selected"]
 4335    );
 4336}
 4337
 4338#[gpui::test]
 4339async fn test_collapse_all_entries_with_invisible_worktree(cx: &mut gpui::TestAppContext) {
 4340    init_test_with_editor(cx);
 4341
 4342    let fs = FakeFs::new(cx.executor());
 4343    fs.insert_tree(
 4344        "/project_root",
 4345        json!({
 4346            "dir_1": {
 4347                "nested_dir": {
 4348                    "file_a.py": "# File contents",
 4349                },
 4350                "file_1.py": "# File contents",
 4351            },
 4352            "dir_2": {
 4353                "file_1.py": "# File contents",
 4354            }
 4355        }),
 4356    )
 4357    .await;
 4358    fs.insert_tree(
 4359        "/external",
 4360        json!({
 4361            "external_file.py": "# External file",
 4362        }),
 4363    )
 4364    .await;
 4365
 4366    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4367    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4368    let workspace = window
 4369        .read_with(cx, |mw, _| mw.workspace().clone())
 4370        .unwrap();
 4371    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4372    let panel = workspace.update_in(cx, ProjectPanel::new);
 4373    cx.run_until_parked();
 4374
 4375    let (_invisible_worktree, _) = project
 4376        .update(cx, |project, cx| {
 4377            project.find_or_create_worktree("/external/external_file.py", false, cx)
 4378        })
 4379        .await
 4380        .unwrap();
 4381    cx.run_until_parked();
 4382
 4383    assert_eq!(
 4384        visible_entries_as_strings(&panel, 0..10, cx),
 4385        &["v project_root", "    > dir_1", "    > dir_2",],
 4386        "invisible worktree should not appear in project panel"
 4387    );
 4388
 4389    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 4390    cx.executor().run_until_parked();
 4391
 4392    panel.update_in(cx, |panel, window, cx| {
 4393        panel.collapse_all_entries(&CollapseAllEntries, window, cx)
 4394    });
 4395    cx.executor().run_until_parked();
 4396    assert_eq!(
 4397        visible_entries_as_strings(&panel, 0..10, cx),
 4398        &["v project_root", "    > dir_1  <== selected", "    > dir_2",],
 4399        "with single visible worktree, root should stay expanded even if invisible worktrees exist"
 4400    );
 4401}
 4402
 4403#[gpui::test]
 4404async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
 4405    init_test(cx);
 4406
 4407    let fs = FakeFs::new(cx.executor());
 4408    fs.as_fake().insert_tree(path!("/root"), json!({})).await;
 4409    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 4410    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4411    let workspace = window
 4412        .read_with(cx, |mw, _| mw.workspace().clone())
 4413        .unwrap();
 4414    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4415    let panel = workspace.update_in(cx, ProjectPanel::new);
 4416    cx.run_until_parked();
 4417
 4418    // Make a new buffer with no backing file
 4419    workspace.update_in(cx, |workspace, window, cx| {
 4420        Editor::new_file(workspace, &Default::default(), window, cx)
 4421    });
 4422
 4423    cx.executor().run_until_parked();
 4424
 4425    // "Save as" the buffer, creating a new backing file for it
 4426    let save_task = workspace.update_in(cx, |workspace, window, cx| {
 4427        workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
 4428    });
 4429
 4430    cx.executor().run_until_parked();
 4431    cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
 4432    save_task.await.unwrap();
 4433
 4434    // Rename the file
 4435    select_path(&panel, "root/new", cx);
 4436    assert_eq!(
 4437        visible_entries_as_strings(&panel, 0..10, cx),
 4438        &["v root", "      new  <== selected  <== marked"]
 4439    );
 4440    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 4441    panel.update_in(cx, |panel, window, cx| {
 4442        panel
 4443            .filename_editor
 4444            .update(cx, |editor, cx| editor.set_text("newer", window, cx));
 4445    });
 4446    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 4447
 4448    cx.executor().run_until_parked();
 4449    assert_eq!(
 4450        visible_entries_as_strings(&panel, 0..10, cx),
 4451        &["v root", "      newer  <== selected"]
 4452    );
 4453
 4454    workspace
 4455        .update_in(cx, |workspace, window, cx| {
 4456            workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
 4457        })
 4458        .await
 4459        .unwrap();
 4460
 4461    cx.executor().run_until_parked();
 4462    // assert that saving the file doesn't restore "new"
 4463    assert_eq!(
 4464        visible_entries_as_strings(&panel, 0..10, cx),
 4465        &["v root", "      newer  <== selected"]
 4466    );
 4467}
 4468
 4469// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
 4470// you can't rename a directory which some program has already open. This is a
 4471// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
 4472// to it, and thus renaming it will fail on Windows.
 4473// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
 4474// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 4475#[gpui::test]
 4476#[cfg_attr(target_os = "windows", ignore)]
 4477async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
 4478    init_test_with_editor(cx);
 4479
 4480    let fs = FakeFs::new(cx.executor());
 4481    fs.insert_tree(
 4482        "/root1",
 4483        json!({
 4484            "dir1": {
 4485                "file1.txt": "content 1",
 4486            },
 4487        }),
 4488    )
 4489    .await;
 4490
 4491    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 4492    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4493    let workspace = window
 4494        .read_with(cx, |mw, _| mw.workspace().clone())
 4495        .unwrap();
 4496    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4497    let panel = workspace.update_in(cx, ProjectPanel::new);
 4498    cx.run_until_parked();
 4499
 4500    toggle_expand_dir(&panel, "root1/dir1", cx);
 4501
 4502    assert_eq!(
 4503        visible_entries_as_strings(&panel, 0..20, cx),
 4504        &["v root1", "    v dir1  <== selected", "          file1.txt",],
 4505        "Initial state with worktrees"
 4506    );
 4507
 4508    select_path(&panel, "root1", cx);
 4509    assert_eq!(
 4510        visible_entries_as_strings(&panel, 0..20, cx),
 4511        &["v root1  <== selected", "    v dir1", "          file1.txt",],
 4512    );
 4513
 4514    // Rename root1 to new_root1
 4515    panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 4516
 4517    assert_eq!(
 4518        visible_entries_as_strings(&panel, 0..20, cx),
 4519        &[
 4520            "v [EDITOR: 'root1']  <== selected",
 4521            "    v dir1",
 4522            "          file1.txt",
 4523        ],
 4524    );
 4525
 4526    let confirm = panel.update_in(cx, |panel, window, cx| {
 4527        panel
 4528            .filename_editor
 4529            .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
 4530        panel.confirm_edit(true, window, cx).unwrap()
 4531    });
 4532    confirm.await.unwrap();
 4533    cx.run_until_parked();
 4534    assert_eq!(
 4535        visible_entries_as_strings(&panel, 0..20, cx),
 4536        &[
 4537            "v new_root1  <== selected",
 4538            "    v dir1",
 4539            "          file1.txt",
 4540        ],
 4541        "Should update worktree name"
 4542    );
 4543
 4544    // Ensure internal paths have been updated
 4545    select_path(&panel, "new_root1/dir1/file1.txt", cx);
 4546    assert_eq!(
 4547        visible_entries_as_strings(&panel, 0..20, cx),
 4548        &[
 4549            "v new_root1",
 4550            "    v dir1",
 4551            "          file1.txt  <== selected",
 4552        ],
 4553        "Files in renamed worktree are selectable"
 4554    );
 4555}
 4556
 4557#[gpui::test]
 4558async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
 4559    init_test_with_editor(cx);
 4560
 4561    let fs = FakeFs::new(cx.executor());
 4562    fs.insert_tree(
 4563        "/root1",
 4564        json!({
 4565            "dir1": { "file1.txt": "content" },
 4566            "file2.txt": "content",
 4567        }),
 4568    )
 4569    .await;
 4570    fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
 4571        .await;
 4572
 4573    // Test 1: Single worktree, hide_root=true - rename should be blocked
 4574    {
 4575        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 4576        let window =
 4577            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4578        let workspace = window
 4579            .read_with(cx, |mw, _| mw.workspace().clone())
 4580            .unwrap();
 4581        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4582
 4583        cx.update(|_, cx| {
 4584            let settings = *ProjectPanelSettings::get_global(cx);
 4585            ProjectPanelSettings::override_global(
 4586                ProjectPanelSettings {
 4587                    hide_root: true,
 4588                    ..settings
 4589                },
 4590                cx,
 4591            );
 4592        });
 4593
 4594        let panel = workspace.update_in(cx, ProjectPanel::new);
 4595        cx.run_until_parked();
 4596
 4597        panel.update(cx, |panel, cx| {
 4598            let project = panel.project.read(cx);
 4599            let worktree = project.visible_worktrees(cx).next().unwrap();
 4600            let root_entry = worktree.read(cx).root_entry().unwrap();
 4601            panel.selection = Some(SelectedEntry {
 4602                worktree_id: worktree.read(cx).id(),
 4603                entry_id: root_entry.id,
 4604            });
 4605        });
 4606
 4607        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 4608
 4609        assert!(
 4610            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
 4611            "Rename should be blocked when hide_root=true with single worktree"
 4612        );
 4613    }
 4614
 4615    // Test 2: Multiple worktrees, hide_root=true - rename should work
 4616    {
 4617        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 4618        let window =
 4619            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4620        let workspace = window
 4621            .read_with(cx, |mw, _| mw.workspace().clone())
 4622            .unwrap();
 4623        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4624
 4625        cx.update(|_, cx| {
 4626            let settings = *ProjectPanelSettings::get_global(cx);
 4627            ProjectPanelSettings::override_global(
 4628                ProjectPanelSettings {
 4629                    hide_root: true,
 4630                    ..settings
 4631                },
 4632                cx,
 4633            );
 4634        });
 4635
 4636        let panel = workspace.update_in(cx, ProjectPanel::new);
 4637        cx.run_until_parked();
 4638
 4639        select_path(&panel, "root1", cx);
 4640        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
 4641
 4642        #[cfg(target_os = "windows")]
 4643        assert!(
 4644            panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
 4645            "Rename should be blocked on Windows even with multiple worktrees"
 4646        );
 4647
 4648        #[cfg(not(target_os = "windows"))]
 4649        {
 4650            assert!(
 4651                panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()),
 4652                "Rename should work with multiple worktrees on non-Windows when hide_root=true"
 4653            );
 4654            panel.update_in(cx, |panel, window, cx| {
 4655                panel.cancel(&menu::Cancel, window, cx)
 4656            });
 4657        }
 4658    }
 4659}
 4660
 4661#[gpui::test]
 4662async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
 4663    init_test_with_editor(cx);
 4664    let fs = FakeFs::new(cx.executor());
 4665    fs.insert_tree(
 4666        "/project_root",
 4667        json!({
 4668            "dir_1": {
 4669                "nested_dir": {
 4670                    "file_a.py": "# File contents",
 4671                }
 4672            },
 4673            "file_1.py": "# File contents",
 4674        }),
 4675    )
 4676    .await;
 4677
 4678    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 4679    let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
 4680    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4681    let workspace = window
 4682        .read_with(cx, |mw, _| mw.workspace().clone())
 4683        .unwrap();
 4684    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4685    let panel = workspace.update_in(cx, ProjectPanel::new);
 4686    cx.run_until_parked();
 4687
 4688    cx.update(|window, cx| {
 4689        panel.update(cx, |this, cx| {
 4690            this.select_next(&Default::default(), window, cx);
 4691            this.expand_selected_entry(&Default::default(), window, cx);
 4692        })
 4693    });
 4694    cx.run_until_parked();
 4695
 4696    cx.update(|window, cx| {
 4697        panel.update(cx, |this, cx| {
 4698            this.expand_selected_entry(&Default::default(), window, cx);
 4699        })
 4700    });
 4701    cx.run_until_parked();
 4702
 4703    cx.update(|window, cx| {
 4704        panel.update(cx, |this, cx| {
 4705            this.select_next(&Default::default(), window, cx);
 4706            this.expand_selected_entry(&Default::default(), window, cx);
 4707        })
 4708    });
 4709    cx.run_until_parked();
 4710
 4711    cx.update(|window, cx| {
 4712        panel.update(cx, |this, cx| {
 4713            this.select_next(&Default::default(), window, cx);
 4714        })
 4715    });
 4716    cx.run_until_parked();
 4717
 4718    assert_eq!(
 4719        visible_entries_as_strings(&panel, 0..10, cx),
 4720        &[
 4721            "v project_root",
 4722            "    v dir_1",
 4723            "        v nested_dir",
 4724            "              file_a.py  <== selected",
 4725            "      file_1.py",
 4726        ]
 4727    );
 4728    let modifiers_with_shift = gpui::Modifiers {
 4729        shift: true,
 4730        ..Default::default()
 4731    };
 4732    cx.run_until_parked();
 4733    cx.simulate_modifiers_change(modifiers_with_shift);
 4734    cx.update(|window, cx| {
 4735        panel.update(cx, |this, cx| {
 4736            this.select_next(&Default::default(), window, cx);
 4737        })
 4738    });
 4739    assert_eq!(
 4740        visible_entries_as_strings(&panel, 0..10, cx),
 4741        &[
 4742            "v project_root",
 4743            "    v dir_1",
 4744            "        v nested_dir",
 4745            "              file_a.py",
 4746            "      file_1.py  <== selected  <== marked",
 4747        ]
 4748    );
 4749    cx.update(|window, cx| {
 4750        panel.update(cx, |this, cx| {
 4751            this.select_previous(&Default::default(), window, cx);
 4752        })
 4753    });
 4754    assert_eq!(
 4755        visible_entries_as_strings(&panel, 0..10, cx),
 4756        &[
 4757            "v project_root",
 4758            "    v dir_1",
 4759            "        v nested_dir",
 4760            "              file_a.py  <== selected  <== marked",
 4761            "      file_1.py  <== marked",
 4762        ]
 4763    );
 4764    cx.update(|window, cx| {
 4765        panel.update(cx, |this, cx| {
 4766            let drag = DraggedSelection {
 4767                active_selection: this.selection.unwrap(),
 4768                marked_selections: this.marked_entries.clone().into(),
 4769            };
 4770            let target_entry = this
 4771                .project
 4772                .read(cx)
 4773                .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
 4774                .unwrap();
 4775            this.drag_onto(&drag, target_entry.id, false, window, cx);
 4776        });
 4777    });
 4778    cx.run_until_parked();
 4779    assert_eq!(
 4780        visible_entries_as_strings(&panel, 0..10, cx),
 4781        &[
 4782            "v project_root",
 4783            "    v dir_1",
 4784            "        v nested_dir",
 4785            "      file_1.py  <== marked",
 4786            "      file_a.py  <== selected  <== marked",
 4787        ]
 4788    );
 4789    // ESC clears out all marks
 4790    cx.update(|window, cx| {
 4791        panel.update(cx, |this, cx| {
 4792            this.cancel(&menu::Cancel, window, cx);
 4793        })
 4794    });
 4795    cx.executor().run_until_parked();
 4796    assert_eq!(
 4797        visible_entries_as_strings(&panel, 0..10, cx),
 4798        &[
 4799            "v project_root",
 4800            "    v dir_1",
 4801            "        v nested_dir",
 4802            "      file_1.py",
 4803            "      file_a.py  <== selected",
 4804        ]
 4805    );
 4806    // ESC clears out all marks
 4807    cx.update(|window, cx| {
 4808        panel.update(cx, |this, cx| {
 4809            this.select_previous(&SelectPrevious, window, cx);
 4810            this.select_next(&SelectNext, window, cx);
 4811        })
 4812    });
 4813    assert_eq!(
 4814        visible_entries_as_strings(&panel, 0..10, cx),
 4815        &[
 4816            "v project_root",
 4817            "    v dir_1",
 4818            "        v nested_dir",
 4819            "      file_1.py  <== marked",
 4820            "      file_a.py  <== selected  <== marked",
 4821        ]
 4822    );
 4823    cx.simulate_modifiers_change(Default::default());
 4824    cx.update(|window, cx| {
 4825        panel.update(cx, |this, cx| {
 4826            this.cut(&Cut, window, cx);
 4827            this.select_previous(&SelectPrevious, window, cx);
 4828            this.select_previous(&SelectPrevious, window, cx);
 4829
 4830            this.paste(&Paste, window, cx);
 4831            this.update_visible_entries(None, false, false, window, cx);
 4832        })
 4833    });
 4834    cx.run_until_parked();
 4835    assert_eq!(
 4836        visible_entries_as_strings(&panel, 0..10, cx),
 4837        &[
 4838            "v project_root",
 4839            "    v dir_1",
 4840            "        v nested_dir",
 4841            "              file_1.py  <== marked",
 4842            "              file_a.py  <== selected  <== marked",
 4843        ]
 4844    );
 4845    cx.simulate_modifiers_change(modifiers_with_shift);
 4846    cx.update(|window, cx| {
 4847        panel.update(cx, |this, cx| {
 4848            this.expand_selected_entry(&Default::default(), window, cx);
 4849            this.select_next(&SelectNext, window, cx);
 4850            this.select_next(&SelectNext, window, cx);
 4851        })
 4852    });
 4853    submit_deletion(&panel, cx);
 4854    assert_eq!(
 4855        visible_entries_as_strings(&panel, 0..10, cx),
 4856        &[
 4857            "v project_root",
 4858            "    v dir_1",
 4859            "        v nested_dir  <== selected",
 4860        ]
 4861    );
 4862}
 4863
 4864#[gpui::test]
 4865async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
 4866    init_test(cx);
 4867
 4868    let fs = FakeFs::new(cx.executor());
 4869    fs.insert_tree(
 4870        "/root",
 4871        json!({
 4872            "a": {
 4873                "b": {
 4874                    "c": {
 4875                        "d": {}
 4876                    }
 4877                }
 4878            },
 4879            "target_destination": {}
 4880        }),
 4881    )
 4882    .await;
 4883
 4884    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 4885    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 4886    let workspace = window
 4887        .read_with(cx, |mw, _| mw.workspace().clone())
 4888        .unwrap();
 4889    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 4890
 4891    cx.update(|_, cx| {
 4892        let settings = *ProjectPanelSettings::get_global(cx);
 4893        ProjectPanelSettings::override_global(
 4894            ProjectPanelSettings {
 4895                auto_fold_dirs: true,
 4896                ..settings
 4897            },
 4898            cx,
 4899        );
 4900    });
 4901
 4902    let panel = workspace.update_in(cx, ProjectPanel::new);
 4903    cx.run_until_parked();
 4904
 4905    // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
 4906    select_path(&panel, "root/a/b/c/d", cx);
 4907    panel.update_in(cx, |panel, window, cx| {
 4908        let drag = DraggedSelection {
 4909            active_selection: *panel.selection.as_ref().unwrap(),
 4910            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
 4911        };
 4912        let target_entry = panel
 4913            .project
 4914            .read(cx)
 4915            .visible_worktrees(cx)
 4916            .next()
 4917            .unwrap()
 4918            .read(cx)
 4919            .entry_for_path(rel_path("target_destination"))
 4920            .unwrap();
 4921        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 4922    });
 4923    cx.executor().run_until_parked();
 4924
 4925    assert_eq!(
 4926        visible_entries_as_strings(&panel, 0..10, cx),
 4927        &[
 4928            "v root",
 4929            "    > a/b/c",
 4930            "    > target_destination/d  <== selected"
 4931        ],
 4932        "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
 4933    );
 4934
 4935    // Reset
 4936    select_path(&panel, "root/target_destination/d", cx);
 4937    panel.update_in(cx, |panel, window, cx| {
 4938        let drag = DraggedSelection {
 4939            active_selection: *panel.selection.as_ref().unwrap(),
 4940            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
 4941        };
 4942        let target_entry = panel
 4943            .project
 4944            .read(cx)
 4945            .visible_worktrees(cx)
 4946            .next()
 4947            .unwrap()
 4948            .read(cx)
 4949            .entry_for_path(rel_path("a/b/c"))
 4950            .unwrap();
 4951        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 4952    });
 4953    cx.executor().run_until_parked();
 4954
 4955    // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
 4956    select_path(&panel, "root/a/b", cx);
 4957    panel.update_in(cx, |panel, window, cx| {
 4958        let drag = DraggedSelection {
 4959            active_selection: *panel.selection.as_ref().unwrap(),
 4960            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
 4961        };
 4962        let target_entry = panel
 4963            .project
 4964            .read(cx)
 4965            .visible_worktrees(cx)
 4966            .next()
 4967            .unwrap()
 4968            .read(cx)
 4969            .entry_for_path(rel_path("target_destination"))
 4970            .unwrap();
 4971        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 4972    });
 4973    cx.executor().run_until_parked();
 4974
 4975    assert_eq!(
 4976        visible_entries_as_strings(&panel, 0..10, cx),
 4977        &["v root", "    v a", "    > target_destination/b/c/d"],
 4978        "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
 4979    );
 4980
 4981    // Reset
 4982    select_path(&panel, "root/target_destination/b", cx);
 4983    panel.update_in(cx, |panel, window, cx| {
 4984        let drag = DraggedSelection {
 4985            active_selection: *panel.selection.as_ref().unwrap(),
 4986            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
 4987        };
 4988        let target_entry = panel
 4989            .project
 4990            .read(cx)
 4991            .visible_worktrees(cx)
 4992            .next()
 4993            .unwrap()
 4994            .read(cx)
 4995            .entry_for_path(rel_path("a"))
 4996            .unwrap();
 4997        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 4998    });
 4999    cx.executor().run_until_parked();
 5000
 5001    // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
 5002    select_path(&panel, "root/a", cx);
 5003    panel.update_in(cx, |panel, window, cx| {
 5004        let drag = DraggedSelection {
 5005            active_selection: *panel.selection.as_ref().unwrap(),
 5006            marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
 5007        };
 5008        let target_entry = panel
 5009            .project
 5010            .read(cx)
 5011            .visible_worktrees(cx)
 5012            .next()
 5013            .unwrap()
 5014            .read(cx)
 5015            .entry_for_path(rel_path("target_destination"))
 5016            .unwrap();
 5017        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 5018    });
 5019    cx.executor().run_until_parked();
 5020
 5021    assert_eq!(
 5022        visible_entries_as_strings(&panel, 0..10, cx),
 5023        &["v root", "    > target_destination/a/b/c/d"],
 5024        "Moving first directory 'a' should move whole 'a/b/c/d' chain"
 5025    );
 5026}
 5027
 5028#[gpui::test]
 5029async fn test_drag_marked_entries_in_folded_directories(cx: &mut gpui::TestAppContext) {
 5030    init_test(cx);
 5031
 5032    let fs = FakeFs::new(cx.executor());
 5033    fs.insert_tree(
 5034        "/root",
 5035        json!({
 5036            "a": {
 5037                "b": {
 5038                    "c": {}
 5039                }
 5040            },
 5041            "e": {
 5042                "f": {
 5043                    "g": {}
 5044                }
 5045            },
 5046            "target": {}
 5047        }),
 5048    )
 5049    .await;
 5050
 5051    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 5052    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5053    let workspace = window
 5054        .read_with(cx, |mw, _| mw.workspace().clone())
 5055        .unwrap();
 5056    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5057
 5058    cx.update(|_, cx| {
 5059        let settings = *ProjectPanelSettings::get_global(cx);
 5060        ProjectPanelSettings::override_global(
 5061            ProjectPanelSettings {
 5062                auto_fold_dirs: true,
 5063                ..settings
 5064            },
 5065            cx,
 5066        );
 5067    });
 5068
 5069    let panel = workspace.update_in(cx, ProjectPanel::new);
 5070    cx.run_until_parked();
 5071
 5072    assert_eq!(
 5073        visible_entries_as_strings(&panel, 0..10, cx),
 5074        &["v root", "    > a/b/c", "    > e/f/g", "    > target"]
 5075    );
 5076
 5077    select_folded_path_with_mark(&panel, "root/a/b/c", "root/a/b", cx);
 5078    select_folded_path_with_mark(&panel, "root/e/f/g", "root/e/f", cx);
 5079
 5080    panel.update_in(cx, |panel, window, cx| {
 5081        let drag = DraggedSelection {
 5082            active_selection: *panel.selection.as_ref().unwrap(),
 5083            marked_selections: panel.marked_entries.clone().into(),
 5084        };
 5085        let target_entry = panel
 5086            .project
 5087            .read(cx)
 5088            .visible_worktrees(cx)
 5089            .next()
 5090            .unwrap()
 5091            .read(cx)
 5092            .entry_for_path(rel_path("target"))
 5093            .unwrap();
 5094        panel.drag_onto(&drag, target_entry.id, false, window, cx);
 5095    });
 5096    cx.executor().run_until_parked();
 5097
 5098    // After dragging 'b/c' and 'f/g' should be moved to target
 5099    assert_eq!(
 5100        visible_entries_as_strings(&panel, 0..10, cx),
 5101        &[
 5102            "v root",
 5103            "    > a",
 5104            "    > e",
 5105            "    v target",
 5106            "        > b/c",
 5107            "        > f/g  <== selected  <== marked"
 5108        ],
 5109        "Should move 'b/c' and 'f/g' to target, leaving 'a' and 'e'"
 5110    );
 5111}
 5112
 5113#[gpui::test]
 5114async fn test_dragging_same_named_files_preserves_one_source_on_conflict(
 5115    cx: &mut gpui::TestAppContext,
 5116) {
 5117    init_test(cx);
 5118
 5119    let fs = FakeFs::new(cx.executor());
 5120    fs.insert_tree(
 5121        "/root",
 5122        json!({
 5123            "dir_a": {
 5124                "shared.txt": "from a"
 5125            },
 5126            "dir_b": {
 5127                "shared.txt": "from b"
 5128            }
 5129        }),
 5130    )
 5131    .await;
 5132
 5133    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 5134    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5135    let workspace = window
 5136        .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
 5137        .unwrap();
 5138    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5139    let panel = workspace.update_in(cx, ProjectPanel::new);
 5140    cx.run_until_parked();
 5141
 5142    panel.update_in(cx, |panel, window, cx| {
 5143        let (root_entry_id, worktree_id, entry_a_id, entry_b_id) = {
 5144            let worktree = panel.project.read(cx).visible_worktrees(cx).next().unwrap();
 5145            let worktree = worktree.read(cx);
 5146            let root_entry_id = worktree.root_entry().unwrap().id;
 5147            let worktree_id = worktree.id();
 5148            let entry_a_id = worktree
 5149                .entry_for_path(rel_path("dir_a/shared.txt"))
 5150                .unwrap()
 5151                .id;
 5152            let entry_b_id = worktree
 5153                .entry_for_path(rel_path("dir_b/shared.txt"))
 5154                .unwrap()
 5155                .id;
 5156            (root_entry_id, worktree_id, entry_a_id, entry_b_id)
 5157        };
 5158
 5159        let drag = DraggedSelection {
 5160            active_selection: SelectedEntry {
 5161                worktree_id,
 5162                entry_id: entry_a_id,
 5163            },
 5164            marked_selections: Arc::new([
 5165                SelectedEntry {
 5166                    worktree_id,
 5167                    entry_id: entry_a_id,
 5168                },
 5169                SelectedEntry {
 5170                    worktree_id,
 5171                    entry_id: entry_b_id,
 5172                },
 5173            ]),
 5174        };
 5175
 5176        panel.drag_onto(&drag, root_entry_id, false, window, cx);
 5177    });
 5178    cx.executor().run_until_parked();
 5179
 5180    let files = fs.files();
 5181    assert!(files.contains(&PathBuf::from(path!("/root/shared.txt"))));
 5182
 5183    let remaining_sources = [
 5184        PathBuf::from(path!("/root/dir_a/shared.txt")),
 5185        PathBuf::from(path!("/root/dir_b/shared.txt")),
 5186    ]
 5187    .into_iter()
 5188    .filter(|path| files.contains(path))
 5189    .count();
 5190
 5191    assert_eq!(
 5192        remaining_sources, 1,
 5193        "one conflicting source file should remain in place"
 5194    );
 5195}
 5196
 5197#[gpui::test]
 5198async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) {
 5199    init_test(cx);
 5200
 5201    let fs = FakeFs::new(cx.executor());
 5202    fs.insert_tree(
 5203        "/root_a",
 5204        json!({
 5205            "src": {
 5206                "lib.rs": "",
 5207                "main.rs": ""
 5208            },
 5209            "docs": {
 5210                "guide.md": ""
 5211            },
 5212            "multi": {
 5213                "alpha.txt": "",
 5214                "beta.txt": ""
 5215            }
 5216        }),
 5217    )
 5218    .await;
 5219    fs.insert_tree(
 5220        "/root_b",
 5221        json!({
 5222            "dst": {
 5223                "existing.md": ""
 5224            },
 5225            "target.txt": ""
 5226        }),
 5227    )
 5228    .await;
 5229
 5230    let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
 5231    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5232    let workspace = window
 5233        .read_with(cx, |mw, _| mw.workspace().clone())
 5234        .unwrap();
 5235    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5236    let panel = workspace.update_in(cx, ProjectPanel::new);
 5237    cx.run_until_parked();
 5238
 5239    // Case 1: move a file onto a directory in another worktree.
 5240    select_path(&panel, "root_a/src/main.rs", cx);
 5241    drag_selection_to(&panel, "root_b/dst", false, cx);
 5242    assert!(
 5243        find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(),
 5244        "Dragged file should appear under destination worktree"
 5245    );
 5246    assert_eq!(
 5247        find_project_entry(&panel, "root_a/src/main.rs", cx),
 5248        None,
 5249        "Dragged file should be removed from the source worktree"
 5250    );
 5251
 5252    // Case 2: drop a file onto another worktree file so it lands in the parent directory.
 5253    select_path(&panel, "root_a/docs/guide.md", cx);
 5254    drag_selection_to(&panel, "root_b/dst/existing.md", true, cx);
 5255    assert!(
 5256        find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(),
 5257        "Dropping onto a file should place the entry beside the target file"
 5258    );
 5259    assert_eq!(
 5260        find_project_entry(&panel, "root_a/docs/guide.md", cx),
 5261        None,
 5262        "Source file should be removed after the move"
 5263    );
 5264
 5265    // Case 3: move an entire directory.
 5266    select_path(&panel, "root_a/src", cx);
 5267    drag_selection_to(&panel, "root_b/dst", false, cx);
 5268    assert!(
 5269        find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(),
 5270        "Dragging a directory should move its nested contents"
 5271    );
 5272    assert_eq!(
 5273        find_project_entry(&panel, "root_a/src", cx),
 5274        None,
 5275        "Directory should no longer exist in the source worktree"
 5276    );
 5277
 5278    // Case 4: multi-selection drag between worktrees.
 5279    panel.update(cx, |panel, _| panel.marked_entries.clear());
 5280    select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx);
 5281    select_path_with_mark(&panel, "root_a/multi/beta.txt", cx);
 5282    drag_selection_to(&panel, "root_b/dst", false, cx);
 5283    assert!(
 5284        find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some()
 5285            && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(),
 5286        "All marked entries should move to the destination worktree"
 5287    );
 5288    assert_eq!(
 5289        find_project_entry(&panel, "root_a/multi/alpha.txt", cx),
 5290        None,
 5291        "Marked entries should be removed from the origin worktree"
 5292    );
 5293    assert_eq!(
 5294        find_project_entry(&panel, "root_a/multi/beta.txt", cx),
 5295        None,
 5296        "Marked entries should be removed from the origin worktree"
 5297    );
 5298}
 5299
 5300#[gpui::test]
 5301async fn test_drag_multiple_entries(cx: &mut gpui::TestAppContext) {
 5302    init_test(cx);
 5303
 5304    let fs = FakeFs::new(cx.executor());
 5305    fs.insert_tree(
 5306        "/root",
 5307        json!({
 5308            "src": {
 5309                "folder1": {
 5310                    "mod.rs": "// folder1 mod"
 5311                },
 5312                "folder2": {
 5313                    "mod.rs": "// folder2 mod"
 5314                },
 5315                "folder3": {
 5316                    "mod.rs": "// folder3 mod",
 5317                    "helper.rs": "// helper"
 5318                },
 5319                "main.rs": ""
 5320            }
 5321        }),
 5322    )
 5323    .await;
 5324
 5325    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 5326    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5327    let workspace = window
 5328        .read_with(cx, |mw, _| mw.workspace().clone())
 5329        .unwrap();
 5330    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5331    let panel = workspace.update_in(cx, ProjectPanel::new);
 5332    cx.run_until_parked();
 5333
 5334    toggle_expand_dir(&panel, "root/src", cx);
 5335    toggle_expand_dir(&panel, "root/src/folder1", cx);
 5336    toggle_expand_dir(&panel, "root/src/folder2", cx);
 5337    toggle_expand_dir(&panel, "root/src/folder3", cx);
 5338    cx.run_until_parked();
 5339
 5340    // Case 1: Dragging a folder and a file from a sibling folder together.
 5341    panel.update(cx, |panel, _| panel.marked_entries.clear());
 5342    select_path_with_mark(&panel, "root/src/folder1", cx);
 5343    select_path_with_mark(&panel, "root/src/folder2/mod.rs", cx);
 5344
 5345    drag_selection_to(&panel, "root", false, cx);
 5346
 5347    assert!(
 5348        find_project_entry(&panel, "root/folder1", cx).is_some(),
 5349        "folder1 should be at root after drag"
 5350    );
 5351    assert!(
 5352        find_project_entry(&panel, "root/folder1/mod.rs", cx).is_some(),
 5353        "folder1/mod.rs should still be inside folder1 after drag"
 5354    );
 5355    assert_eq!(
 5356        find_project_entry(&panel, "root/src/folder1", cx),
 5357        None,
 5358        "folder1 should no longer be in src"
 5359    );
 5360    assert!(
 5361        find_project_entry(&panel, "root/mod.rs", cx).is_some(),
 5362        "mod.rs from folder2 should be at root"
 5363    );
 5364
 5365    // Case 2: Dragging a folder and its own child together.
 5366    panel.update(cx, |panel, _| panel.marked_entries.clear());
 5367    select_path_with_mark(&panel, "root/src/folder3", cx);
 5368    select_path_with_mark(&panel, "root/src/folder3/mod.rs", cx);
 5369
 5370    drag_selection_to(&panel, "root", false, cx);
 5371
 5372    assert!(
 5373        find_project_entry(&panel, "root/folder3", cx).is_some(),
 5374        "folder3 should be at root after drag"
 5375    );
 5376    assert!(
 5377        find_project_entry(&panel, "root/folder3/mod.rs", cx).is_some(),
 5378        "folder3/mod.rs should still be inside folder3"
 5379    );
 5380    assert!(
 5381        find_project_entry(&panel, "root/folder3/helper.rs", cx).is_some(),
 5382        "folder3/helper.rs should still be inside folder3"
 5383    );
 5384}
 5385
 5386#[gpui::test]
 5387async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
 5388    init_test_with_editor(cx);
 5389    cx.update(|cx| {
 5390        cx.update_global::<SettingsStore, _>(|store, cx| {
 5391            store.update_user_settings(cx, |settings| {
 5392                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
 5393                settings
 5394                    .project_panel
 5395                    .get_or_insert_default()
 5396                    .auto_reveal_entries = Some(false);
 5397            });
 5398        })
 5399    });
 5400
 5401    let fs = FakeFs::new(cx.background_executor.clone());
 5402    fs.insert_tree(
 5403        "/project_root",
 5404        json!({
 5405            ".git": {},
 5406            ".gitignore": "**/gitignored_dir",
 5407            "dir_1": {
 5408                "file_1.py": "# File 1_1 contents",
 5409                "file_2.py": "# File 1_2 contents",
 5410                "file_3.py": "# File 1_3 contents",
 5411                "gitignored_dir": {
 5412                    "file_a.py": "# File contents",
 5413                    "file_b.py": "# File contents",
 5414                    "file_c.py": "# File contents",
 5415                },
 5416            },
 5417            "dir_2": {
 5418                "file_1.py": "# File 2_1 contents",
 5419                "file_2.py": "# File 2_2 contents",
 5420                "file_3.py": "# File 2_3 contents",
 5421            }
 5422        }),
 5423    )
 5424    .await;
 5425
 5426    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 5427    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5428    let workspace = window
 5429        .read_with(cx, |mw, _| mw.workspace().clone())
 5430        .unwrap();
 5431    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5432    let panel = workspace.update_in(cx, ProjectPanel::new);
 5433    cx.run_until_parked();
 5434
 5435    assert_eq!(
 5436        visible_entries_as_strings(&panel, 0..20, cx),
 5437        &[
 5438            "v project_root",
 5439            "    > .git",
 5440            "    > dir_1",
 5441            "    > dir_2",
 5442            "      .gitignore",
 5443        ]
 5444    );
 5445
 5446    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
 5447        .expect("dir 1 file is not ignored and should have an entry");
 5448    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
 5449        .expect("dir 2 file is not ignored and should have an entry");
 5450    let gitignored_dir_file =
 5451        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
 5452    assert_eq!(
 5453        gitignored_dir_file, None,
 5454        "File in the gitignored dir should not have an entry before its dir is toggled"
 5455    );
 5456
 5457    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 5458    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
 5459    cx.executor().run_until_parked();
 5460    assert_eq!(
 5461        visible_entries_as_strings(&panel, 0..20, cx),
 5462        &[
 5463            "v project_root",
 5464            "    > .git",
 5465            "    v dir_1",
 5466            "        v gitignored_dir  <== selected",
 5467            "              file_a.py",
 5468            "              file_b.py",
 5469            "              file_c.py",
 5470            "          file_1.py",
 5471            "          file_2.py",
 5472            "          file_3.py",
 5473            "    > dir_2",
 5474            "      .gitignore",
 5475        ],
 5476        "Should show gitignored dir file list in the project panel"
 5477    );
 5478    let gitignored_dir_file =
 5479        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
 5480            .expect("after gitignored dir got opened, a file entry should be present");
 5481
 5482    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
 5483    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 5484    assert_eq!(
 5485        visible_entries_as_strings(&panel, 0..20, cx),
 5486        &[
 5487            "v project_root",
 5488            "    > .git",
 5489            "    > dir_1  <== selected",
 5490            "    > dir_2",
 5491            "      .gitignore",
 5492        ],
 5493        "Should hide all dir contents again and prepare for the auto reveal test"
 5494    );
 5495
 5496    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
 5497        panel.update(cx, |panel, cx| {
 5498            panel.project.update(cx, |_, cx| {
 5499                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
 5500            })
 5501        });
 5502        cx.run_until_parked();
 5503        assert_eq!(
 5504            visible_entries_as_strings(&panel, 0..20, cx),
 5505            &[
 5506                "v project_root",
 5507                "    > .git",
 5508                "    > dir_1  <== selected",
 5509                "    > dir_2",
 5510                "      .gitignore",
 5511            ],
 5512            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
 5513        );
 5514    }
 5515
 5516    cx.update(|_, cx| {
 5517        cx.update_global::<SettingsStore, _>(|store, cx| {
 5518            store.update_user_settings(cx, |settings| {
 5519                settings
 5520                    .project_panel
 5521                    .get_or_insert_default()
 5522                    .auto_reveal_entries = Some(true)
 5523            });
 5524        })
 5525    });
 5526
 5527    panel.update(cx, |panel, cx| {
 5528        panel.project.update(cx, |_, cx| {
 5529            cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
 5530        })
 5531    });
 5532    cx.run_until_parked();
 5533    assert_eq!(
 5534        visible_entries_as_strings(&panel, 0..20, cx),
 5535        &[
 5536            "v project_root",
 5537            "    > .git",
 5538            "    v dir_1",
 5539            "        > gitignored_dir",
 5540            "          file_1.py  <== selected  <== marked",
 5541            "          file_2.py",
 5542            "          file_3.py",
 5543            "    > dir_2",
 5544            "      .gitignore",
 5545        ],
 5546        "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
 5547    );
 5548
 5549    panel.update(cx, |panel, cx| {
 5550        panel.project.update(cx, |_, cx| {
 5551            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
 5552        })
 5553    });
 5554    cx.run_until_parked();
 5555    assert_eq!(
 5556        visible_entries_as_strings(&panel, 0..20, cx),
 5557        &[
 5558            "v project_root",
 5559            "    > .git",
 5560            "    v dir_1",
 5561            "        > gitignored_dir",
 5562            "          file_1.py",
 5563            "          file_2.py",
 5564            "          file_3.py",
 5565            "    v dir_2",
 5566            "          file_1.py  <== selected  <== marked",
 5567            "          file_2.py",
 5568            "          file_3.py",
 5569            "      .gitignore",
 5570        ],
 5571        "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
 5572    );
 5573
 5574    panel.update(cx, |panel, cx| {
 5575        panel.project.update(cx, |_, cx| {
 5576            cx.emit(project::Event::ActiveEntryChanged(Some(
 5577                gitignored_dir_file,
 5578            )))
 5579        })
 5580    });
 5581    cx.run_until_parked();
 5582    assert_eq!(
 5583        visible_entries_as_strings(&panel, 0..20, cx),
 5584        &[
 5585            "v project_root",
 5586            "    > .git",
 5587            "    v dir_1",
 5588            "        > gitignored_dir",
 5589            "          file_1.py",
 5590            "          file_2.py",
 5591            "          file_3.py",
 5592            "    v dir_2",
 5593            "          file_1.py  <== selected  <== marked",
 5594            "          file_2.py",
 5595            "          file_3.py",
 5596            "      .gitignore",
 5597        ],
 5598        "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
 5599    );
 5600
 5601    panel.update(cx, |panel, cx| {
 5602        panel.project.update(cx, |_, cx| {
 5603            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
 5604        })
 5605    });
 5606    cx.run_until_parked();
 5607    assert_eq!(
 5608        visible_entries_as_strings(&panel, 0..20, cx),
 5609        &[
 5610            "v project_root",
 5611            "    > .git",
 5612            "    v dir_1",
 5613            "        v gitignored_dir",
 5614            "              file_a.py  <== selected  <== marked",
 5615            "              file_b.py",
 5616            "              file_c.py",
 5617            "          file_1.py",
 5618            "          file_2.py",
 5619            "          file_3.py",
 5620            "    v dir_2",
 5621            "          file_1.py",
 5622            "          file_2.py",
 5623            "          file_3.py",
 5624            "      .gitignore",
 5625        ],
 5626        "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
 5627    );
 5628
 5629    panel.update(cx, |panel, cx| {
 5630        panel.project.update(cx, |_, cx| {
 5631            cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
 5632        })
 5633    });
 5634    cx.run_until_parked();
 5635    assert_eq!(
 5636        visible_entries_as_strings(&panel, 0..20, cx),
 5637        &[
 5638            "v project_root",
 5639            "    > .git",
 5640            "    v dir_1",
 5641            "        v gitignored_dir",
 5642            "              file_a.py",
 5643            "              file_b.py",
 5644            "              file_c.py",
 5645            "          file_1.py",
 5646            "          file_2.py",
 5647            "          file_3.py",
 5648            "    v dir_2",
 5649            "          file_1.py  <== selected  <== marked",
 5650            "          file_2.py",
 5651            "          file_3.py",
 5652            "      .gitignore",
 5653        ],
 5654        "After switching to dir_2_file, it should be selected and marked"
 5655    );
 5656
 5657    panel.update(cx, |panel, cx| {
 5658        panel.project.update(cx, |_, cx| {
 5659            cx.emit(project::Event::ActiveEntryChanged(Some(
 5660                gitignored_dir_file,
 5661            )))
 5662        })
 5663    });
 5664    cx.run_until_parked();
 5665    assert_eq!(
 5666        visible_entries_as_strings(&panel, 0..20, cx),
 5667        &[
 5668            "v project_root",
 5669            "    > .git",
 5670            "    v dir_1",
 5671            "        v gitignored_dir",
 5672            "              file_a.py  <== selected  <== marked",
 5673            "              file_b.py",
 5674            "              file_c.py",
 5675            "          file_1.py",
 5676            "          file_2.py",
 5677            "          file_3.py",
 5678            "    v dir_2",
 5679            "          file_1.py",
 5680            "          file_2.py",
 5681            "          file_3.py",
 5682            "      .gitignore",
 5683        ],
 5684        "When a gitignored entry is already visible, auto reveal should mark it as selected"
 5685    );
 5686}
 5687
 5688#[gpui::test]
 5689async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
 5690    init_test_with_editor(cx);
 5691    cx.update(|cx| {
 5692        cx.update_global::<SettingsStore, _>(|store, cx| {
 5693            store.update_user_settings(cx, |settings| {
 5694                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
 5695                settings.project.worktree.file_scan_inclusions =
 5696                    Some(vec!["always_included_but_ignored_dir/*".to_string()]);
 5697                settings
 5698                    .project_panel
 5699                    .get_or_insert_default()
 5700                    .auto_reveal_entries = Some(false)
 5701            });
 5702        })
 5703    });
 5704
 5705    let fs = FakeFs::new(cx.background_executor.clone());
 5706    fs.insert_tree(
 5707        "/project_root",
 5708        json!({
 5709            ".git": {},
 5710            ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
 5711            "dir_1": {
 5712                "file_1.py": "# File 1_1 contents",
 5713                "file_2.py": "# File 1_2 contents",
 5714                "file_3.py": "# File 1_3 contents",
 5715                "gitignored_dir": {
 5716                    "file_a.py": "# File contents",
 5717                    "file_b.py": "# File contents",
 5718                    "file_c.py": "# File contents",
 5719                },
 5720            },
 5721            "dir_2": {
 5722                "file_1.py": "# File 2_1 contents",
 5723                "file_2.py": "# File 2_2 contents",
 5724                "file_3.py": "# File 2_3 contents",
 5725            },
 5726            "always_included_but_ignored_dir": {
 5727                "file_a.py": "# File contents",
 5728                "file_b.py": "# File contents",
 5729                "file_c.py": "# File contents",
 5730            },
 5731        }),
 5732    )
 5733    .await;
 5734
 5735    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 5736    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5737    let workspace = window
 5738        .read_with(cx, |mw, _| mw.workspace().clone())
 5739        .unwrap();
 5740    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5741    let panel = workspace.update_in(cx, ProjectPanel::new);
 5742    cx.run_until_parked();
 5743
 5744    assert_eq!(
 5745        visible_entries_as_strings(&panel, 0..20, cx),
 5746        &[
 5747            "v project_root",
 5748            "    > .git",
 5749            "    > always_included_but_ignored_dir",
 5750            "    > dir_1",
 5751            "    > dir_2",
 5752            "      .gitignore",
 5753        ]
 5754    );
 5755
 5756    let gitignored_dir_file =
 5757        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
 5758    let always_included_but_ignored_dir_file = find_project_entry(
 5759        &panel,
 5760        "project_root/always_included_but_ignored_dir/file_a.py",
 5761        cx,
 5762    )
 5763    .expect("file that is .gitignored but set to always be included should have an entry");
 5764    assert_eq!(
 5765        gitignored_dir_file, None,
 5766        "File in the gitignored dir should not have an entry unless its directory is toggled"
 5767    );
 5768
 5769    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 5770    cx.run_until_parked();
 5771    cx.update(|_, cx| {
 5772        cx.update_global::<SettingsStore, _>(|store, cx| {
 5773            store.update_user_settings(cx, |settings| {
 5774                settings
 5775                    .project_panel
 5776                    .get_or_insert_default()
 5777                    .auto_reveal_entries = Some(true)
 5778            });
 5779        })
 5780    });
 5781
 5782    panel.update(cx, |panel, cx| {
 5783        panel.project.update(cx, |_, cx| {
 5784            cx.emit(project::Event::ActiveEntryChanged(Some(
 5785                always_included_but_ignored_dir_file,
 5786            )))
 5787        })
 5788    });
 5789    cx.run_until_parked();
 5790
 5791    assert_eq!(
 5792        visible_entries_as_strings(&panel, 0..20, cx),
 5793        &[
 5794            "v project_root",
 5795            "    > .git",
 5796            "    v always_included_but_ignored_dir",
 5797            "          file_a.py  <== selected  <== marked",
 5798            "          file_b.py",
 5799            "          file_c.py",
 5800            "    v dir_1",
 5801            "        > gitignored_dir",
 5802            "          file_1.py",
 5803            "          file_2.py",
 5804            "          file_3.py",
 5805            "    > dir_2",
 5806            "      .gitignore",
 5807        ],
 5808        "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
 5809    );
 5810}
 5811
 5812#[gpui::test]
 5813async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
 5814    init_test_with_editor(cx);
 5815    cx.update(|cx| {
 5816        cx.update_global::<SettingsStore, _>(|store, cx| {
 5817            store.update_user_settings(cx, |settings| {
 5818                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
 5819                settings
 5820                    .project_panel
 5821                    .get_or_insert_default()
 5822                    .auto_reveal_entries = Some(false)
 5823            });
 5824        })
 5825    });
 5826
 5827    let fs = FakeFs::new(cx.background_executor.clone());
 5828    fs.insert_tree(
 5829        "/project_root",
 5830        json!({
 5831            ".git": {},
 5832            ".gitignore": "**/gitignored_dir",
 5833            "dir_1": {
 5834                "file_1.py": "# File 1_1 contents",
 5835                "file_2.py": "# File 1_2 contents",
 5836                "file_3.py": "# File 1_3 contents",
 5837                "gitignored_dir": {
 5838                    "file_a.py": "# File contents",
 5839                    "file_b.py": "# File contents",
 5840                    "file_c.py": "# File contents",
 5841                },
 5842            },
 5843            "dir_2": {
 5844                "file_1.py": "# File 2_1 contents",
 5845                "file_2.py": "# File 2_2 contents",
 5846                "file_3.py": "# File 2_3 contents",
 5847            }
 5848        }),
 5849    )
 5850    .await;
 5851
 5852    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
 5853    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 5854    let workspace = window
 5855        .read_with(cx, |mw, _| mw.workspace().clone())
 5856        .unwrap();
 5857    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 5858    let panel = workspace.update_in(cx, ProjectPanel::new);
 5859    cx.run_until_parked();
 5860
 5861    assert_eq!(
 5862        visible_entries_as_strings(&panel, 0..20, cx),
 5863        &[
 5864            "v project_root",
 5865            "    > .git",
 5866            "    > dir_1",
 5867            "    > dir_2",
 5868            "      .gitignore",
 5869        ]
 5870    );
 5871
 5872    let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
 5873        .expect("dir 1 file is not ignored and should have an entry");
 5874    let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
 5875        .expect("dir 2 file is not ignored and should have an entry");
 5876    let gitignored_dir_file =
 5877        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
 5878    assert_eq!(
 5879        gitignored_dir_file, None,
 5880        "File in the gitignored dir should not have an entry before its dir is toggled"
 5881    );
 5882
 5883    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 5884    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
 5885    cx.run_until_parked();
 5886    assert_eq!(
 5887        visible_entries_as_strings(&panel, 0..20, cx),
 5888        &[
 5889            "v project_root",
 5890            "    > .git",
 5891            "    v dir_1",
 5892            "        v gitignored_dir  <== selected",
 5893            "              file_a.py",
 5894            "              file_b.py",
 5895            "              file_c.py",
 5896            "          file_1.py",
 5897            "          file_2.py",
 5898            "          file_3.py",
 5899            "    > dir_2",
 5900            "      .gitignore",
 5901        ],
 5902        "Should show gitignored dir file list in the project panel"
 5903    );
 5904    let gitignored_dir_file =
 5905        find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
 5906            .expect("after gitignored dir got opened, a file entry should be present");
 5907
 5908    toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
 5909    toggle_expand_dir(&panel, "project_root/dir_1", cx);
 5910    assert_eq!(
 5911        visible_entries_as_strings(&panel, 0..20, cx),
 5912        &[
 5913            "v project_root",
 5914            "    > .git",
 5915            "    > dir_1  <== selected",
 5916            "    > dir_2",
 5917            "      .gitignore",
 5918        ],
 5919        "Should hide all dir contents again and prepare for the explicit reveal test"
 5920    );
 5921
 5922    for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
 5923        panel.update(cx, |panel, cx| {
 5924            panel.project.update(cx, |_, cx| {
 5925                cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
 5926            })
 5927        });
 5928        cx.run_until_parked();
 5929        assert_eq!(
 5930            visible_entries_as_strings(&panel, 0..20, cx),
 5931            &[
 5932                "v project_root",
 5933                "    > .git",
 5934                "    > dir_1  <== selected",
 5935                "    > dir_2",
 5936                "      .gitignore",
 5937            ],
 5938            "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
 5939        );
 5940    }
 5941
 5942    panel.update(cx, |panel, cx| {
 5943        panel.project.update(cx, |_, cx| {
 5944            cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
 5945        })
 5946    });
 5947    cx.run_until_parked();
 5948    assert_eq!(
 5949        visible_entries_as_strings(&panel, 0..20, cx),
 5950        &[
 5951            "v project_root",
 5952            "    > .git",
 5953            "    v dir_1",
 5954            "        > gitignored_dir",
 5955            "          file_1.py  <== selected  <== marked",
 5956            "          file_2.py",
 5957            "          file_3.py",
 5958            "    > dir_2",
 5959            "      .gitignore",
 5960        ],
 5961        "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
 5962    );
 5963
 5964    panel.update(cx, |panel, cx| {
 5965        panel.project.update(cx, |_, cx| {
 5966            cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
 5967        })
 5968    });
 5969    cx.run_until_parked();
 5970    assert_eq!(
 5971        visible_entries_as_strings(&panel, 0..20, cx),
 5972        &[
 5973            "v project_root",
 5974            "    > .git",
 5975            "    v dir_1",
 5976            "        > gitignored_dir",
 5977            "          file_1.py",
 5978            "          file_2.py",
 5979            "          file_3.py",
 5980            "    v dir_2",
 5981            "          file_1.py  <== selected  <== marked",
 5982            "          file_2.py",
 5983            "          file_3.py",
 5984            "      .gitignore",
 5985        ],
 5986        "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
 5987    );
 5988
 5989    panel.update(cx, |panel, cx| {
 5990        panel.project.update(cx, |_, cx| {
 5991            cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
 5992        })
 5993    });
 5994    cx.run_until_parked();
 5995    assert_eq!(
 5996        visible_entries_as_strings(&panel, 0..20, cx),
 5997        &[
 5998            "v project_root",
 5999            "    > .git",
 6000            "    v dir_1",
 6001            "        v gitignored_dir",
 6002            "              file_a.py  <== selected  <== marked",
 6003            "              file_b.py",
 6004            "              file_c.py",
 6005            "          file_1.py",
 6006            "          file_2.py",
 6007            "          file_3.py",
 6008            "    v dir_2",
 6009            "          file_1.py",
 6010            "          file_2.py",
 6011            "          file_3.py",
 6012            "      .gitignore",
 6013        ],
 6014        "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
 6015    );
 6016}
 6017
 6018#[gpui::test]
 6019async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
 6020    init_test(cx);
 6021    cx.update(|cx| {
 6022        cx.update_global::<SettingsStore, _>(|store, cx| {
 6023            store.update_user_settings(cx, |settings| {
 6024                settings.project.worktree.file_scan_exclusions =
 6025                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
 6026            });
 6027        });
 6028    });
 6029
 6030    cx.update(|cx| {
 6031        register_project_item::<TestProjectItemView>(cx);
 6032    });
 6033
 6034    let fs = FakeFs::new(cx.executor());
 6035    fs.insert_tree(
 6036        "/root1",
 6037        json!({
 6038            ".dockerignore": "",
 6039            ".git": {
 6040                "HEAD": "",
 6041            },
 6042        }),
 6043    )
 6044    .await;
 6045
 6046    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 6047    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6048    let workspace = window
 6049        .read_with(cx, |mw, _| mw.workspace().clone())
 6050        .unwrap();
 6051    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6052    let panel = workspace.update_in(cx, |workspace, window, cx| {
 6053        let panel = ProjectPanel::new(workspace, window, cx);
 6054        workspace.add_panel(panel.clone(), window, cx);
 6055        panel
 6056    });
 6057    cx.run_until_parked();
 6058
 6059    select_path(&panel, "root1", cx);
 6060    assert_eq!(
 6061        visible_entries_as_strings(&panel, 0..10, cx),
 6062        &["v root1  <== selected", "      .dockerignore",]
 6063    );
 6064    workspace.update_in(cx, |workspace, _, cx| {
 6065        assert!(
 6066            workspace.active_item(cx).is_none(),
 6067            "Should have no active items in the beginning"
 6068        );
 6069    });
 6070
 6071    let excluded_file_path = ".git/COMMIT_EDITMSG";
 6072    let excluded_dir_path = "excluded_dir";
 6073
 6074    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 6075    cx.run_until_parked();
 6076    panel.update_in(cx, |panel, window, cx| {
 6077        assert!(panel.filename_editor.read(cx).is_focused(window));
 6078    });
 6079    panel
 6080        .update_in(cx, |panel, window, cx| {
 6081            panel.filename_editor.update(cx, |editor, cx| {
 6082                editor.set_text(excluded_file_path, window, cx)
 6083            });
 6084            panel.confirm_edit(true, window, cx).unwrap()
 6085        })
 6086        .await
 6087        .unwrap();
 6088
 6089    assert_eq!(
 6090        visible_entries_as_strings(&panel, 0..13, cx),
 6091        &["v root1", "      .dockerignore"],
 6092        "Excluded dir should not be shown after opening a file in it"
 6093    );
 6094    panel.update_in(cx, |panel, window, cx| {
 6095        assert!(
 6096            !panel.filename_editor.read(cx).is_focused(window),
 6097            "Should have closed the file name editor"
 6098        );
 6099    });
 6100    workspace.update_in(cx, |workspace, _, cx| {
 6101        let active_entry_path = workspace
 6102            .active_item(cx)
 6103            .expect("should have opened and activated the excluded item")
 6104            .act_as::<TestProjectItemView>(cx)
 6105            .expect("should have opened the corresponding project item for the excluded item")
 6106            .read(cx)
 6107            .path
 6108            .clone();
 6109        assert_eq!(
 6110            active_entry_path.path.as_ref(),
 6111            rel_path(excluded_file_path),
 6112            "Should open the excluded file"
 6113        );
 6114
 6115        assert!(
 6116            workspace.notification_ids().is_empty(),
 6117            "Should have no notifications after opening an excluded file"
 6118        );
 6119    });
 6120    assert!(
 6121        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
 6122        "Should have created the excluded file"
 6123    );
 6124
 6125    select_path(&panel, "root1", cx);
 6126    panel.update_in(cx, |panel, window, cx| {
 6127        panel.new_directory(&NewDirectory, window, cx)
 6128    });
 6129    cx.run_until_parked();
 6130    panel.update_in(cx, |panel, window, cx| {
 6131        assert!(panel.filename_editor.read(cx).is_focused(window));
 6132    });
 6133    panel
 6134        .update_in(cx, |panel, window, cx| {
 6135            panel.filename_editor.update(cx, |editor, cx| {
 6136                editor.set_text(excluded_file_path, window, cx)
 6137            });
 6138            panel.confirm_edit(true, window, cx).unwrap()
 6139        })
 6140        .await
 6141        .unwrap();
 6142    cx.run_until_parked();
 6143    assert_eq!(
 6144        visible_entries_as_strings(&panel, 0..13, cx),
 6145        &["v root1", "      .dockerignore"],
 6146        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
 6147    );
 6148    panel.update_in(cx, |panel, window, cx| {
 6149        assert!(
 6150            !panel.filename_editor.read(cx).is_focused(window),
 6151            "Should have closed the file name editor"
 6152        );
 6153    });
 6154    workspace.update_in(cx, |workspace, _, cx| {
 6155        let notifications = workspace.notification_ids();
 6156        assert_eq!(
 6157            notifications.len(),
 6158            1,
 6159            "Should receive one notification with the error message"
 6160        );
 6161        workspace.dismiss_notification(notifications.first().unwrap(), cx);
 6162        assert!(workspace.notification_ids().is_empty());
 6163    });
 6164
 6165    select_path(&panel, "root1", cx);
 6166    panel.update_in(cx, |panel, window, cx| {
 6167        panel.new_directory(&NewDirectory, window, cx)
 6168    });
 6169    cx.run_until_parked();
 6170
 6171    panel.update_in(cx, |panel, window, cx| {
 6172        assert!(panel.filename_editor.read(cx).is_focused(window));
 6173    });
 6174
 6175    panel
 6176        .update_in(cx, |panel, window, cx| {
 6177            panel.filename_editor.update(cx, |editor, cx| {
 6178                editor.set_text(excluded_dir_path, window, cx)
 6179            });
 6180            panel.confirm_edit(true, window, cx).unwrap()
 6181        })
 6182        .await
 6183        .unwrap();
 6184
 6185    cx.run_until_parked();
 6186
 6187    assert_eq!(
 6188        visible_entries_as_strings(&panel, 0..13, cx),
 6189        &["v root1", "      .dockerignore"],
 6190        "Should not change the project panel after trying to create an excluded directory"
 6191    );
 6192    panel.update_in(cx, |panel, window, cx| {
 6193        assert!(
 6194            !panel.filename_editor.read(cx).is_focused(window),
 6195            "Should have closed the file name editor"
 6196        );
 6197    });
 6198    workspace.update_in(cx, |workspace, _, cx| {
 6199        let notifications = workspace.notification_ids();
 6200        assert_eq!(
 6201            notifications.len(),
 6202            1,
 6203            "Should receive one notification explaining that no directory is actually shown"
 6204        );
 6205        workspace.dismiss_notification(notifications.first().unwrap(), cx);
 6206        assert!(workspace.notification_ids().is_empty());
 6207    });
 6208    assert!(
 6209        fs.is_dir(Path::new("/root1/excluded_dir")).await,
 6210        "Should have created the excluded directory"
 6211    );
 6212}
 6213
 6214#[gpui::test]
 6215async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
 6216    init_test_with_editor(cx);
 6217
 6218    let fs = FakeFs::new(cx.executor());
 6219    fs.insert_tree(
 6220        "/src",
 6221        json!({
 6222            "test": {
 6223                "first.rs": "// First Rust file",
 6224                "second.rs": "// Second Rust file",
 6225                "third.rs": "// Third Rust file",
 6226            }
 6227        }),
 6228    )
 6229    .await;
 6230
 6231    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
 6232    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6233    let workspace = window
 6234        .read_with(cx, |mw, _| mw.workspace().clone())
 6235        .unwrap();
 6236    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6237    let panel = workspace.update_in(cx, |workspace, window, cx| {
 6238        let panel = ProjectPanel::new(workspace, window, cx);
 6239        workspace.add_panel(panel.clone(), window, cx);
 6240        panel
 6241    });
 6242    cx.run_until_parked();
 6243
 6244    select_path(&panel, "src", cx);
 6245    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 6246    cx.executor().run_until_parked();
 6247    assert_eq!(
 6248        visible_entries_as_strings(&panel, 0..10, cx),
 6249        &[
 6250            //
 6251            "v src  <== selected",
 6252            "    > test"
 6253        ]
 6254    );
 6255    panel.update_in(cx, |panel, window, cx| {
 6256        panel.new_directory(&NewDirectory, window, cx)
 6257    });
 6258    cx.executor().run_until_parked();
 6259    panel.update_in(cx, |panel, window, cx| {
 6260        assert!(panel.filename_editor.read(cx).is_focused(window));
 6261    });
 6262    assert_eq!(
 6263        visible_entries_as_strings(&panel, 0..10, cx),
 6264        &[
 6265            //
 6266            "v src",
 6267            "    > [EDITOR: '']  <== selected",
 6268            "    > test"
 6269        ]
 6270    );
 6271
 6272    panel.update_in(cx, |panel, window, cx| {
 6273        panel.cancel(&menu::Cancel, window, cx);
 6274    });
 6275    cx.executor().run_until_parked();
 6276    assert_eq!(
 6277        visible_entries_as_strings(&panel, 0..10, cx),
 6278        &[
 6279            //
 6280            "v src  <== selected",
 6281            "    > test"
 6282        ]
 6283    );
 6284
 6285    panel.update_in(cx, |panel, window, cx| {
 6286        panel.new_directory(&NewDirectory, window, cx)
 6287    });
 6288    cx.executor().run_until_parked();
 6289    panel.update_in(cx, |panel, window, cx| {
 6290        assert!(panel.filename_editor.read(cx).is_focused(window));
 6291    });
 6292    assert_eq!(
 6293        visible_entries_as_strings(&panel, 0..10, cx),
 6294        &[
 6295            //
 6296            "v src",
 6297            "    > [EDITOR: '']  <== selected",
 6298            "    > test"
 6299        ]
 6300    );
 6301    workspace.update_in(cx, |_, window, _| window.blur());
 6302    cx.executor().run_until_parked();
 6303    assert_eq!(
 6304        visible_entries_as_strings(&panel, 0..10, cx),
 6305        &[
 6306            //
 6307            "v src  <== selected",
 6308            "    > test"
 6309        ]
 6310    );
 6311}
 6312
 6313#[gpui::test]
 6314async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
 6315    init_test_with_editor(cx);
 6316
 6317    let fs = FakeFs::new(cx.executor());
 6318    fs.insert_tree(
 6319        "/root",
 6320        json!({
 6321            "dir1": {
 6322                "subdir1": {},
 6323                "file1.txt": "",
 6324                "file2.txt": "",
 6325            },
 6326            "dir2": {
 6327                "subdir2": {},
 6328                "file3.txt": "",
 6329                "file4.txt": "",
 6330            },
 6331            "file5.txt": "",
 6332            "file6.txt": "",
 6333        }),
 6334    )
 6335    .await;
 6336
 6337    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6338    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6339    let workspace = window
 6340        .read_with(cx, |mw, _| mw.workspace().clone())
 6341        .unwrap();
 6342    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6343    let panel = workspace.update_in(cx, ProjectPanel::new);
 6344    cx.run_until_parked();
 6345
 6346    toggle_expand_dir(&panel, "root/dir1", cx);
 6347    toggle_expand_dir(&panel, "root/dir2", cx);
 6348
 6349    // Test Case 1: Delete middle file in directory
 6350    select_path(&panel, "root/dir1/file1.txt", cx);
 6351    assert_eq!(
 6352        visible_entries_as_strings(&panel, 0..15, cx),
 6353        &[
 6354            "v root",
 6355            "    v dir1",
 6356            "        > subdir1",
 6357            "          file1.txt  <== selected",
 6358            "          file2.txt",
 6359            "    v dir2",
 6360            "        > subdir2",
 6361            "          file3.txt",
 6362            "          file4.txt",
 6363            "      file5.txt",
 6364            "      file6.txt",
 6365        ],
 6366        "Initial state before deleting middle file"
 6367    );
 6368
 6369    submit_deletion(&panel, cx);
 6370    assert_eq!(
 6371        visible_entries_as_strings(&panel, 0..15, cx),
 6372        &[
 6373            "v root",
 6374            "    v dir1",
 6375            "        > subdir1",
 6376            "          file2.txt  <== selected",
 6377            "    v dir2",
 6378            "        > subdir2",
 6379            "          file3.txt",
 6380            "          file4.txt",
 6381            "      file5.txt",
 6382            "      file6.txt",
 6383        ],
 6384        "Should select next file after deleting middle file"
 6385    );
 6386
 6387    // Test Case 2: Delete last file in directory
 6388    submit_deletion(&panel, cx);
 6389    assert_eq!(
 6390        visible_entries_as_strings(&panel, 0..15, cx),
 6391        &[
 6392            "v root",
 6393            "    v dir1",
 6394            "        > subdir1  <== selected",
 6395            "    v dir2",
 6396            "        > subdir2",
 6397            "          file3.txt",
 6398            "          file4.txt",
 6399            "      file5.txt",
 6400            "      file6.txt",
 6401        ],
 6402        "Should select next directory when last file is deleted"
 6403    );
 6404
 6405    // Test Case 3: Delete root level file
 6406    select_path(&panel, "root/file6.txt", cx);
 6407    assert_eq!(
 6408        visible_entries_as_strings(&panel, 0..15, cx),
 6409        &[
 6410            "v root",
 6411            "    v dir1",
 6412            "        > subdir1",
 6413            "    v dir2",
 6414            "        > subdir2",
 6415            "          file3.txt",
 6416            "          file4.txt",
 6417            "      file5.txt",
 6418            "      file6.txt  <== selected",
 6419        ],
 6420        "Initial state before deleting root level file"
 6421    );
 6422
 6423    submit_deletion(&panel, cx);
 6424    assert_eq!(
 6425        visible_entries_as_strings(&panel, 0..15, cx),
 6426        &[
 6427            "v root",
 6428            "    v dir1",
 6429            "        > subdir1",
 6430            "    v dir2",
 6431            "        > subdir2",
 6432            "          file3.txt",
 6433            "          file4.txt",
 6434            "      file5.txt  <== selected",
 6435        ],
 6436        "Should select prev entry at root level"
 6437    );
 6438}
 6439
 6440#[gpui::test]
 6441async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
 6442    init_test_with_editor(cx);
 6443
 6444    let fs = FakeFs::new(cx.executor());
 6445    fs.insert_tree(
 6446        path!("/root"),
 6447        json!({
 6448            "aa": "// Testing 1",
 6449            "bb": "// Testing 2",
 6450            "cc": "// Testing 3",
 6451            "dd": "// Testing 4",
 6452            "ee": "// Testing 5",
 6453            "ff": "// Testing 6",
 6454            "gg": "// Testing 7",
 6455            "hh": "// Testing 8",
 6456            "ii": "// Testing 8",
 6457            ".gitignore": "bb\ndd\nee\nff\nii\n'",
 6458        }),
 6459    )
 6460    .await;
 6461
 6462    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 6463    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6464    let workspace = window
 6465        .read_with(cx, |mw, _| mw.workspace().clone())
 6466        .unwrap();
 6467    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6468
 6469    // Test 1: Auto selection with one gitignored file next to the deleted file
 6470    cx.update(|_, cx| {
 6471        let settings = *ProjectPanelSettings::get_global(cx);
 6472        ProjectPanelSettings::override_global(
 6473            ProjectPanelSettings {
 6474                hide_gitignore: true,
 6475                ..settings
 6476            },
 6477            cx,
 6478        );
 6479    });
 6480
 6481    let panel = workspace.update_in(cx, ProjectPanel::new);
 6482    cx.run_until_parked();
 6483
 6484    select_path(&panel, "root/aa", cx);
 6485    assert_eq!(
 6486        visible_entries_as_strings(&panel, 0..10, cx),
 6487        &[
 6488            "v root",
 6489            "      .gitignore",
 6490            "      aa  <== selected",
 6491            "      cc",
 6492            "      gg",
 6493            "      hh"
 6494        ],
 6495        "Initial state should hide files on .gitignore"
 6496    );
 6497
 6498    submit_deletion(&panel, cx);
 6499
 6500    assert_eq!(
 6501        visible_entries_as_strings(&panel, 0..10, cx),
 6502        &[
 6503            "v root",
 6504            "      .gitignore",
 6505            "      cc  <== selected",
 6506            "      gg",
 6507            "      hh"
 6508        ],
 6509        "Should select next entry not on .gitignore"
 6510    );
 6511
 6512    // Test 2: Auto selection with many gitignored files next to the deleted file
 6513    submit_deletion(&panel, cx);
 6514    assert_eq!(
 6515        visible_entries_as_strings(&panel, 0..10, cx),
 6516        &[
 6517            "v root",
 6518            "      .gitignore",
 6519            "      gg  <== selected",
 6520            "      hh"
 6521        ],
 6522        "Should select next entry not on .gitignore"
 6523    );
 6524
 6525    // Test 3: Auto selection of entry before deleted file
 6526    select_path(&panel, "root/hh", cx);
 6527    assert_eq!(
 6528        visible_entries_as_strings(&panel, 0..10, cx),
 6529        &[
 6530            "v root",
 6531            "      .gitignore",
 6532            "      gg",
 6533            "      hh  <== selected"
 6534        ],
 6535        "Should select next entry not on .gitignore"
 6536    );
 6537    submit_deletion(&panel, cx);
 6538    assert_eq!(
 6539        visible_entries_as_strings(&panel, 0..10, cx),
 6540        &["v root", "      .gitignore", "      gg  <== selected"],
 6541        "Should select next entry not on .gitignore"
 6542    );
 6543}
 6544
 6545#[gpui::test]
 6546async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
 6547    init_test_with_editor(cx);
 6548
 6549    let fs = FakeFs::new(cx.executor());
 6550    fs.insert_tree(
 6551        path!("/root"),
 6552        json!({
 6553            "dir1": {
 6554                "file1": "// Testing",
 6555                "file2": "// Testing",
 6556                "file3": "// Testing"
 6557            },
 6558            "aa": "// Testing",
 6559            ".gitignore": "file1\nfile3\n",
 6560        }),
 6561    )
 6562    .await;
 6563
 6564    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 6565    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6566    let workspace = window
 6567        .read_with(cx, |mw, _| mw.workspace().clone())
 6568        .unwrap();
 6569    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6570
 6571    cx.update(|_, cx| {
 6572        let settings = *ProjectPanelSettings::get_global(cx);
 6573        ProjectPanelSettings::override_global(
 6574            ProjectPanelSettings {
 6575                hide_gitignore: true,
 6576                ..settings
 6577            },
 6578            cx,
 6579        );
 6580    });
 6581
 6582    let panel = workspace.update_in(cx, ProjectPanel::new);
 6583    cx.run_until_parked();
 6584
 6585    // Test 1: Visible items should exclude files on gitignore
 6586    toggle_expand_dir(&panel, "root/dir1", cx);
 6587    select_path(&panel, "root/dir1/file2", cx);
 6588    assert_eq!(
 6589        visible_entries_as_strings(&panel, 0..10, cx),
 6590        &[
 6591            "v root",
 6592            "    v dir1",
 6593            "          file2  <== selected",
 6594            "      .gitignore",
 6595            "      aa"
 6596        ],
 6597        "Initial state should hide files on .gitignore"
 6598    );
 6599    submit_deletion(&panel, cx);
 6600
 6601    // Test 2: Auto selection should go to the parent
 6602    assert_eq!(
 6603        visible_entries_as_strings(&panel, 0..10, cx),
 6604        &[
 6605            "v root",
 6606            "    v dir1  <== selected",
 6607            "      .gitignore",
 6608            "      aa"
 6609        ],
 6610        "Initial state should hide files on .gitignore"
 6611    );
 6612}
 6613
 6614#[gpui::test]
 6615async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
 6616    init_test_with_editor(cx);
 6617
 6618    let fs = FakeFs::new(cx.executor());
 6619    fs.insert_tree(
 6620        "/root",
 6621        json!({
 6622            "dir1": {
 6623                "subdir1": {
 6624                    "a.txt": "",
 6625                    "b.txt": ""
 6626                },
 6627                "file1.txt": "",
 6628            },
 6629            "dir2": {
 6630                "subdir2": {
 6631                    "c.txt": "",
 6632                    "d.txt": ""
 6633                },
 6634                "file2.txt": "",
 6635            },
 6636            "file3.txt": "",
 6637        }),
 6638    )
 6639    .await;
 6640
 6641    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6642    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6643    let workspace = window
 6644        .read_with(cx, |mw, _| mw.workspace().clone())
 6645        .unwrap();
 6646    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6647    let panel = workspace.update_in(cx, ProjectPanel::new);
 6648    cx.run_until_parked();
 6649
 6650    toggle_expand_dir(&panel, "root/dir1", cx);
 6651    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 6652    toggle_expand_dir(&panel, "root/dir2", cx);
 6653    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
 6654
 6655    // Test Case 1: Select and delete nested directory with parent
 6656    cx.simulate_modifiers_change(gpui::Modifiers {
 6657        control: true,
 6658        ..Default::default()
 6659    });
 6660    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
 6661    select_path_with_mark(&panel, "root/dir1", cx);
 6662
 6663    assert_eq!(
 6664        visible_entries_as_strings(&panel, 0..15, cx),
 6665        &[
 6666            "v root",
 6667            "    v dir1  <== selected  <== marked",
 6668            "        v subdir1  <== marked",
 6669            "              a.txt",
 6670            "              b.txt",
 6671            "          file1.txt",
 6672            "    v dir2",
 6673            "        v subdir2",
 6674            "              c.txt",
 6675            "              d.txt",
 6676            "          file2.txt",
 6677            "      file3.txt",
 6678        ],
 6679        "Initial state before deleting nested directory with parent"
 6680    );
 6681
 6682    submit_deletion(&panel, cx);
 6683    assert_eq!(
 6684        visible_entries_as_strings(&panel, 0..15, cx),
 6685        &[
 6686            "v root",
 6687            "    v dir2  <== selected",
 6688            "        v subdir2",
 6689            "              c.txt",
 6690            "              d.txt",
 6691            "          file2.txt",
 6692            "      file3.txt",
 6693        ],
 6694        "Should select next directory after deleting directory with parent"
 6695    );
 6696
 6697    // Test Case 2: Select mixed files and directories across levels
 6698    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
 6699    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
 6700    select_path_with_mark(&panel, "root/file3.txt", cx);
 6701
 6702    assert_eq!(
 6703        visible_entries_as_strings(&panel, 0..15, cx),
 6704        &[
 6705            "v root",
 6706            "    v dir2",
 6707            "        v subdir2",
 6708            "              c.txt  <== marked",
 6709            "              d.txt",
 6710            "          file2.txt  <== marked",
 6711            "      file3.txt  <== selected  <== marked",
 6712        ],
 6713        "Initial state before deleting"
 6714    );
 6715
 6716    submit_deletion(&panel, cx);
 6717    assert_eq!(
 6718        visible_entries_as_strings(&panel, 0..15, cx),
 6719        &[
 6720            "v root",
 6721            "    v dir2  <== selected",
 6722            "        v subdir2",
 6723            "              d.txt",
 6724        ],
 6725        "Should select sibling directory"
 6726    );
 6727}
 6728
 6729#[gpui::test]
 6730async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
 6731    init_test_with_editor(cx);
 6732
 6733    let fs = FakeFs::new(cx.executor());
 6734    fs.insert_tree(
 6735        "/root",
 6736        json!({
 6737            "dir1": {
 6738                "subdir1": {
 6739                    "a.txt": "",
 6740                    "b.txt": ""
 6741                },
 6742                "file1.txt": "",
 6743            },
 6744            "dir2": {
 6745                "subdir2": {
 6746                    "c.txt": "",
 6747                    "d.txt": ""
 6748                },
 6749                "file2.txt": "",
 6750            },
 6751            "file3.txt": "",
 6752            "file4.txt": "",
 6753        }),
 6754    )
 6755    .await;
 6756
 6757    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6758    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6759    let workspace = window
 6760        .read_with(cx, |mw, _| mw.workspace().clone())
 6761        .unwrap();
 6762    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6763    let panel = workspace.update_in(cx, ProjectPanel::new);
 6764    cx.run_until_parked();
 6765
 6766    toggle_expand_dir(&panel, "root/dir1", cx);
 6767    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 6768    toggle_expand_dir(&panel, "root/dir2", cx);
 6769    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
 6770
 6771    // Test Case 1: Select all root files and directories
 6772    cx.simulate_modifiers_change(gpui::Modifiers {
 6773        control: true,
 6774        ..Default::default()
 6775    });
 6776    select_path_with_mark(&panel, "root/dir1", cx);
 6777    select_path_with_mark(&panel, "root/dir2", cx);
 6778    select_path_with_mark(&panel, "root/file3.txt", cx);
 6779    select_path_with_mark(&panel, "root/file4.txt", cx);
 6780    assert_eq!(
 6781        visible_entries_as_strings(&panel, 0..20, cx),
 6782        &[
 6783            "v root",
 6784            "    v dir1  <== marked",
 6785            "        v subdir1",
 6786            "              a.txt",
 6787            "              b.txt",
 6788            "          file1.txt",
 6789            "    v dir2  <== marked",
 6790            "        v subdir2",
 6791            "              c.txt",
 6792            "              d.txt",
 6793            "          file2.txt",
 6794            "      file3.txt  <== marked",
 6795            "      file4.txt  <== selected  <== marked",
 6796        ],
 6797        "State before deleting all contents"
 6798    );
 6799
 6800    submit_deletion(&panel, cx);
 6801    assert_eq!(
 6802        visible_entries_as_strings(&panel, 0..20, cx),
 6803        &["v root  <== selected"],
 6804        "Only empty root directory should remain after deleting all contents"
 6805    );
 6806}
 6807
 6808#[gpui::test]
 6809async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
 6810    init_test_with_editor(cx);
 6811
 6812    let fs = FakeFs::new(cx.executor());
 6813    fs.insert_tree(
 6814        "/root",
 6815        json!({
 6816            "dir1": {
 6817                "subdir1": {
 6818                    "file_a.txt": "content a",
 6819                    "file_b.txt": "content b",
 6820                },
 6821                "subdir2": {
 6822                    "file_c.txt": "content c",
 6823                },
 6824                "file1.txt": "content 1",
 6825            },
 6826            "dir2": {
 6827                "file2.txt": "content 2",
 6828            },
 6829        }),
 6830    )
 6831    .await;
 6832
 6833    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6834    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6835    let workspace = window
 6836        .read_with(cx, |mw, _| mw.workspace().clone())
 6837        .unwrap();
 6838    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6839    let panel = workspace.update_in(cx, ProjectPanel::new);
 6840    cx.run_until_parked();
 6841
 6842    toggle_expand_dir(&panel, "root/dir1", cx);
 6843    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 6844    toggle_expand_dir(&panel, "root/dir2", cx);
 6845    cx.simulate_modifiers_change(gpui::Modifiers {
 6846        control: true,
 6847        ..Default::default()
 6848    });
 6849
 6850    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
 6851    select_path_with_mark(&panel, "root/dir1", cx);
 6852    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
 6853    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
 6854
 6855    assert_eq!(
 6856        visible_entries_as_strings(&panel, 0..20, cx),
 6857        &[
 6858            "v root",
 6859            "    v dir1  <== marked",
 6860            "        v subdir1  <== marked",
 6861            "              file_a.txt  <== selected  <== marked",
 6862            "              file_b.txt",
 6863            "        > subdir2",
 6864            "          file1.txt",
 6865            "    v dir2",
 6866            "          file2.txt",
 6867        ],
 6868        "State with parent dir, subdir, and file selected"
 6869    );
 6870    submit_deletion(&panel, cx);
 6871    assert_eq!(
 6872        visible_entries_as_strings(&panel, 0..20, cx),
 6873        &["v root", "    v dir2  <== selected", "          file2.txt",],
 6874        "Only dir2 should remain after deletion"
 6875    );
 6876}
 6877
 6878#[gpui::test]
 6879async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
 6880    init_test_with_editor(cx);
 6881
 6882    let fs = FakeFs::new(cx.executor());
 6883    // First worktree
 6884    fs.insert_tree(
 6885        "/root1",
 6886        json!({
 6887            "dir1": {
 6888                "file1.txt": "content 1",
 6889                "file2.txt": "content 2",
 6890            },
 6891            "dir2": {
 6892                "file3.txt": "content 3",
 6893            },
 6894        }),
 6895    )
 6896    .await;
 6897
 6898    // Second worktree
 6899    fs.insert_tree(
 6900        "/root2",
 6901        json!({
 6902            "dir3": {
 6903                "file4.txt": "content 4",
 6904                "file5.txt": "content 5",
 6905            },
 6906            "file6.txt": "content 6",
 6907        }),
 6908    )
 6909    .await;
 6910
 6911    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 6912    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6913    let workspace = window
 6914        .read_with(cx, |mw, _| mw.workspace().clone())
 6915        .unwrap();
 6916    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6917    let panel = workspace.update_in(cx, ProjectPanel::new);
 6918    cx.run_until_parked();
 6919
 6920    // Expand all directories for testing
 6921    toggle_expand_dir(&panel, "root1/dir1", cx);
 6922    toggle_expand_dir(&panel, "root1/dir2", cx);
 6923    toggle_expand_dir(&panel, "root2/dir3", cx);
 6924
 6925    // Test Case 1: Delete files across different worktrees
 6926    cx.simulate_modifiers_change(gpui::Modifiers {
 6927        control: true,
 6928        ..Default::default()
 6929    });
 6930    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
 6931    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
 6932
 6933    assert_eq!(
 6934        visible_entries_as_strings(&panel, 0..20, cx),
 6935        &[
 6936            "v root1",
 6937            "    v dir1",
 6938            "          file1.txt  <== marked",
 6939            "          file2.txt",
 6940            "    v dir2",
 6941            "          file3.txt",
 6942            "v root2",
 6943            "    v dir3",
 6944            "          file4.txt  <== selected  <== marked",
 6945            "          file5.txt",
 6946            "      file6.txt",
 6947        ],
 6948        "Initial state with files selected from different worktrees"
 6949    );
 6950
 6951    submit_deletion(&panel, cx);
 6952    assert_eq!(
 6953        visible_entries_as_strings(&panel, 0..20, cx),
 6954        &[
 6955            "v root1",
 6956            "    v dir1",
 6957            "          file2.txt",
 6958            "    v dir2",
 6959            "          file3.txt",
 6960            "v root2",
 6961            "    v dir3",
 6962            "          file5.txt  <== selected",
 6963            "      file6.txt",
 6964        ],
 6965        "Should select next file in the last worktree after deletion"
 6966    );
 6967
 6968    // Test Case 2: Delete directories from different worktrees
 6969    select_path_with_mark(&panel, "root1/dir1", cx);
 6970    select_path_with_mark(&panel, "root2/dir3", cx);
 6971
 6972    assert_eq!(
 6973        visible_entries_as_strings(&panel, 0..20, cx),
 6974        &[
 6975            "v root1",
 6976            "    v dir1  <== marked",
 6977            "          file2.txt",
 6978            "    v dir2",
 6979            "          file3.txt",
 6980            "v root2",
 6981            "    v dir3  <== selected  <== marked",
 6982            "          file5.txt",
 6983            "      file6.txt",
 6984        ],
 6985        "State with directories marked from different worktrees"
 6986    );
 6987
 6988    submit_deletion(&panel, cx);
 6989    assert_eq!(
 6990        visible_entries_as_strings(&panel, 0..20, cx),
 6991        &[
 6992            "v root1",
 6993            "    v dir2",
 6994            "          file3.txt",
 6995            "v root2",
 6996            "      file6.txt  <== selected",
 6997        ],
 6998        "Should select remaining file in last worktree after directory deletion"
 6999    );
 7000
 7001    // Test Case 4: Delete all remaining files except roots
 7002    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
 7003    select_path_with_mark(&panel, "root2/file6.txt", cx);
 7004
 7005    assert_eq!(
 7006        visible_entries_as_strings(&panel, 0..20, cx),
 7007        &[
 7008            "v root1",
 7009            "    v dir2",
 7010            "          file3.txt  <== marked",
 7011            "v root2",
 7012            "      file6.txt  <== selected  <== marked",
 7013        ],
 7014        "State with all remaining files marked"
 7015    );
 7016
 7017    submit_deletion(&panel, cx);
 7018    assert_eq!(
 7019        visible_entries_as_strings(&panel, 0..20, cx),
 7020        &["v root1", "    v dir2", "v root2  <== selected"],
 7021        "Second parent root should be selected after deleting"
 7022    );
 7023}
 7024
 7025#[gpui::test]
 7026async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
 7027    init_test_with_editor(cx);
 7028
 7029    let fs = FakeFs::new(cx.executor());
 7030    fs.insert_tree(
 7031        "/root",
 7032        json!({
 7033            "dir1": {
 7034                "file1.txt": "",
 7035                "file2.txt": "",
 7036                "file3.txt": "",
 7037            },
 7038            "dir2": {
 7039                "file4.txt": "",
 7040                "file5.txt": "",
 7041            },
 7042        }),
 7043    )
 7044    .await;
 7045
 7046    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 7047    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7048    let workspace = window
 7049        .read_with(cx, |mw, _| mw.workspace().clone())
 7050        .unwrap();
 7051    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7052    let panel = workspace.update_in(cx, ProjectPanel::new);
 7053    cx.run_until_parked();
 7054
 7055    toggle_expand_dir(&panel, "root/dir1", cx);
 7056    toggle_expand_dir(&panel, "root/dir2", cx);
 7057
 7058    cx.simulate_modifiers_change(gpui::Modifiers {
 7059        control: true,
 7060        ..Default::default()
 7061    });
 7062
 7063    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
 7064    select_path(&panel, "root/dir1/file1.txt", cx);
 7065
 7066    assert_eq!(
 7067        visible_entries_as_strings(&panel, 0..15, cx),
 7068        &[
 7069            "v root",
 7070            "    v dir1",
 7071            "          file1.txt  <== selected",
 7072            "          file2.txt  <== marked",
 7073            "          file3.txt",
 7074            "    v dir2",
 7075            "          file4.txt",
 7076            "          file5.txt",
 7077        ],
 7078        "Initial state with one marked entry and different selection"
 7079    );
 7080
 7081    // Delete should operate on the selected entry (file1.txt)
 7082    submit_deletion(&panel, cx);
 7083    assert_eq!(
 7084        visible_entries_as_strings(&panel, 0..15, cx),
 7085        &[
 7086            "v root",
 7087            "    v dir1",
 7088            "          file2.txt  <== selected  <== marked",
 7089            "          file3.txt",
 7090            "    v dir2",
 7091            "          file4.txt",
 7092            "          file5.txt",
 7093        ],
 7094        "Should delete selected file, not marked file"
 7095    );
 7096
 7097    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
 7098    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
 7099    select_path(&panel, "root/dir2/file5.txt", cx);
 7100
 7101    assert_eq!(
 7102        visible_entries_as_strings(&panel, 0..15, cx),
 7103        &[
 7104            "v root",
 7105            "    v dir1",
 7106            "          file2.txt  <== marked",
 7107            "          file3.txt  <== marked",
 7108            "    v dir2",
 7109            "          file4.txt  <== marked",
 7110            "          file5.txt  <== selected",
 7111        ],
 7112        "Initial state with multiple marked entries and different selection"
 7113    );
 7114
 7115    // Delete should operate on all marked entries, ignoring the selection
 7116    submit_deletion(&panel, cx);
 7117    assert_eq!(
 7118        visible_entries_as_strings(&panel, 0..15, cx),
 7119        &[
 7120            "v root",
 7121            "    v dir1",
 7122            "    v dir2",
 7123            "          file5.txt  <== selected",
 7124        ],
 7125        "Should delete all marked files, leaving only the selected file"
 7126    );
 7127}
 7128
 7129#[gpui::test]
 7130async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
 7131    init_test_with_editor(cx);
 7132
 7133    let fs = FakeFs::new(cx.executor());
 7134    fs.insert_tree(
 7135        "/root_b",
 7136        json!({
 7137            "dir1": {
 7138                "file1.txt": "content 1",
 7139                "file2.txt": "content 2",
 7140            },
 7141        }),
 7142    )
 7143    .await;
 7144
 7145    fs.insert_tree(
 7146        "/root_c",
 7147        json!({
 7148            "dir2": {},
 7149        }),
 7150    )
 7151    .await;
 7152
 7153    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
 7154    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7155    let workspace = window
 7156        .read_with(cx, |mw, _| mw.workspace().clone())
 7157        .unwrap();
 7158    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7159    let panel = workspace.update_in(cx, ProjectPanel::new);
 7160    cx.run_until_parked();
 7161
 7162    toggle_expand_dir(&panel, "root_b/dir1", cx);
 7163    toggle_expand_dir(&panel, "root_c/dir2", cx);
 7164
 7165    cx.simulate_modifiers_change(gpui::Modifiers {
 7166        control: true,
 7167        ..Default::default()
 7168    });
 7169    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
 7170    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
 7171
 7172    assert_eq!(
 7173        visible_entries_as_strings(&panel, 0..20, cx),
 7174        &[
 7175            "v root_b",
 7176            "    v dir1",
 7177            "          file1.txt  <== marked",
 7178            "          file2.txt  <== selected  <== marked",
 7179            "v root_c",
 7180            "    v dir2",
 7181        ],
 7182        "Initial state with files marked in root_b"
 7183    );
 7184
 7185    submit_deletion(&panel, cx);
 7186    assert_eq!(
 7187        visible_entries_as_strings(&panel, 0..20, cx),
 7188        &[
 7189            "v root_b",
 7190            "    v dir1  <== selected",
 7191            "v root_c",
 7192            "    v dir2",
 7193        ],
 7194        "After deletion in root_b as it's last deletion, selection should be in root_b"
 7195    );
 7196
 7197    select_path_with_mark(&panel, "root_c/dir2", cx);
 7198
 7199    submit_deletion(&panel, cx);
 7200    assert_eq!(
 7201        visible_entries_as_strings(&panel, 0..20, cx),
 7202        &["v root_b", "    v dir1", "v root_c  <== selected",],
 7203        "After deleting from root_c, it should remain in root_c"
 7204    );
 7205}
 7206
 7207fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 7208    let path = rel_path(path);
 7209    panel.update_in(cx, |panel, window, cx| {
 7210        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 7211            let worktree = worktree.read(cx);
 7212            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 7213                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 7214                panel.toggle_expanded(entry_id, window, cx);
 7215                return;
 7216            }
 7217        }
 7218        panic!("no worktree for path {:?}", path);
 7219    });
 7220    cx.run_until_parked();
 7221}
 7222
 7223#[gpui::test]
 7224async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
 7225    init_test_with_editor(cx);
 7226
 7227    let fs = FakeFs::new(cx.executor());
 7228    fs.insert_tree(
 7229        path!("/root"),
 7230        json!({
 7231            ".gitignore": "**/ignored_dir\n**/ignored_nested",
 7232            "dir1": {
 7233                "empty1": {
 7234                    "empty2": {
 7235                        "empty3": {
 7236                            "file.txt": ""
 7237                        }
 7238                    }
 7239                },
 7240                "subdir1": {
 7241                    "file1.txt": "",
 7242                    "file2.txt": "",
 7243                    "ignored_nested": {
 7244                        "ignored_file.txt": ""
 7245                    }
 7246                },
 7247                "ignored_dir": {
 7248                    "subdir": {
 7249                        "deep_file.txt": ""
 7250                    }
 7251                }
 7252            }
 7253        }),
 7254    )
 7255    .await;
 7256
 7257    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7258    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7259    let workspace = window
 7260        .read_with(cx, |mw, _| mw.workspace().clone())
 7261        .unwrap();
 7262    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7263
 7264    // Test 1: When auto-fold is enabled
 7265    cx.update(|_, cx| {
 7266        let settings = *ProjectPanelSettings::get_global(cx);
 7267        ProjectPanelSettings::override_global(
 7268            ProjectPanelSettings {
 7269                auto_fold_dirs: true,
 7270                ..settings
 7271            },
 7272            cx,
 7273        );
 7274    });
 7275
 7276    let panel = workspace.update_in(cx, ProjectPanel::new);
 7277    cx.run_until_parked();
 7278
 7279    assert_eq!(
 7280        visible_entries_as_strings(&panel, 0..20, cx),
 7281        &["v root", "    > dir1", "      .gitignore",],
 7282        "Initial state should show collapsed root structure"
 7283    );
 7284
 7285    toggle_expand_dir(&panel, "root/dir1", cx);
 7286    assert_eq!(
 7287        visible_entries_as_strings(&panel, 0..20, cx),
 7288        &[
 7289            "v root",
 7290            "    v dir1  <== selected",
 7291            "        > empty1/empty2/empty3",
 7292            "        > ignored_dir",
 7293            "        > subdir1",
 7294            "      .gitignore",
 7295        ],
 7296        "Should show first level with auto-folded dirs and ignored dir visible"
 7297    );
 7298
 7299    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7300    panel.update_in(cx, |panel, window, cx| {
 7301        let project = panel.project.read(cx);
 7302        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7303        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
 7304        panel.update_visible_entries(None, false, false, window, cx);
 7305    });
 7306    cx.run_until_parked();
 7307
 7308    assert_eq!(
 7309        visible_entries_as_strings(&panel, 0..20, cx),
 7310        &[
 7311            "v root",
 7312            "    v dir1  <== selected",
 7313            "        v empty1",
 7314            "            v empty2",
 7315            "                v empty3",
 7316            "                      file.txt",
 7317            "        > ignored_dir",
 7318            "        v subdir1",
 7319            "            > ignored_nested",
 7320            "              file1.txt",
 7321            "              file2.txt",
 7322            "      .gitignore",
 7323        ],
 7324        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
 7325    );
 7326
 7327    // Test 2: When auto-fold is disabled
 7328    cx.update(|_, cx| {
 7329        let settings = *ProjectPanelSettings::get_global(cx);
 7330        ProjectPanelSettings::override_global(
 7331            ProjectPanelSettings {
 7332                auto_fold_dirs: false,
 7333                ..settings
 7334            },
 7335            cx,
 7336        );
 7337    });
 7338
 7339    panel.update_in(cx, |panel, window, cx| {
 7340        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
 7341    });
 7342
 7343    toggle_expand_dir(&panel, "root/dir1", cx);
 7344    assert_eq!(
 7345        visible_entries_as_strings(&panel, 0..20, cx),
 7346        &[
 7347            "v root",
 7348            "    v dir1  <== selected",
 7349            "        > empty1",
 7350            "        > ignored_dir",
 7351            "        > subdir1",
 7352            "      .gitignore",
 7353        ],
 7354        "With auto-fold disabled: should show all directories separately"
 7355    );
 7356
 7357    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7358    panel.update_in(cx, |panel, window, cx| {
 7359        let project = panel.project.read(cx);
 7360        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7361        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
 7362        panel.update_visible_entries(None, false, false, window, cx);
 7363    });
 7364    cx.run_until_parked();
 7365
 7366    assert_eq!(
 7367        visible_entries_as_strings(&panel, 0..20, cx),
 7368        &[
 7369            "v root",
 7370            "    v dir1  <== selected",
 7371            "        v empty1",
 7372            "            v empty2",
 7373            "                v empty3",
 7374            "                      file.txt",
 7375            "        > ignored_dir",
 7376            "        v subdir1",
 7377            "            > ignored_nested",
 7378            "              file1.txt",
 7379            "              file2.txt",
 7380            "      .gitignore",
 7381        ],
 7382        "After expand_all without auto-fold: should expand all dirs normally, \
 7383         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
 7384    );
 7385
 7386    // Test 3: When explicitly called on ignored directory
 7387    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
 7388    panel.update_in(cx, |panel, window, cx| {
 7389        let project = panel.project.read(cx);
 7390        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7391        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
 7392        panel.update_visible_entries(None, false, false, window, cx);
 7393    });
 7394    cx.run_until_parked();
 7395
 7396    assert_eq!(
 7397        visible_entries_as_strings(&panel, 0..20, cx),
 7398        &[
 7399            "v root",
 7400            "    v dir1  <== selected",
 7401            "        v empty1",
 7402            "            v empty2",
 7403            "                v empty3",
 7404            "                      file.txt",
 7405            "        v ignored_dir",
 7406            "            v subdir",
 7407            "                  deep_file.txt",
 7408            "        v subdir1",
 7409            "            > ignored_nested",
 7410            "              file1.txt",
 7411            "              file2.txt",
 7412            "      .gitignore",
 7413        ],
 7414        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
 7415    );
 7416}
 7417
 7418#[gpui::test]
 7419async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
 7420    init_test(cx);
 7421
 7422    let fs = FakeFs::new(cx.executor());
 7423    fs.insert_tree(
 7424        path!("/root"),
 7425        json!({
 7426            "dir1": {
 7427                "subdir1": {
 7428                    "nested1": {
 7429                        "file1.txt": "",
 7430                        "file2.txt": ""
 7431                    },
 7432                },
 7433                "subdir2": {
 7434                    "file4.txt": ""
 7435                }
 7436            },
 7437            "dir2": {
 7438                "single_file": {
 7439                    "file5.txt": ""
 7440                }
 7441            }
 7442        }),
 7443    )
 7444    .await;
 7445
 7446    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7447    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7448    let workspace = window
 7449        .read_with(cx, |mw, _| mw.workspace().clone())
 7450        .unwrap();
 7451    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7452
 7453    // Test 1: Basic collapsing
 7454    {
 7455        let panel = workspace.update_in(cx, ProjectPanel::new);
 7456        cx.run_until_parked();
 7457
 7458        toggle_expand_dir(&panel, "root/dir1", cx);
 7459        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7460        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7461        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
 7462
 7463        assert_eq!(
 7464            visible_entries_as_strings(&panel, 0..20, cx),
 7465            &[
 7466                "v root",
 7467                "    v dir1",
 7468                "        v subdir1",
 7469                "            v nested1",
 7470                "                  file1.txt",
 7471                "                  file2.txt",
 7472                "        v subdir2  <== selected",
 7473                "              file4.txt",
 7474                "    > dir2",
 7475            ],
 7476            "Initial state with everything expanded"
 7477        );
 7478
 7479        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7480        panel.update_in(cx, |panel, window, cx| {
 7481            let project = panel.project.read(cx);
 7482            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7483            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7484            panel.update_visible_entries(None, false, false, window, cx);
 7485        });
 7486        cx.run_until_parked();
 7487
 7488        assert_eq!(
 7489            visible_entries_as_strings(&panel, 0..20, cx),
 7490            &["v root", "    > dir1", "    > dir2",],
 7491            "All subdirs under dir1 should be collapsed"
 7492        );
 7493    }
 7494
 7495    // Test 2: With auto-fold enabled
 7496    {
 7497        cx.update(|_, cx| {
 7498            let settings = *ProjectPanelSettings::get_global(cx);
 7499            ProjectPanelSettings::override_global(
 7500                ProjectPanelSettings {
 7501                    auto_fold_dirs: true,
 7502                    ..settings
 7503                },
 7504                cx,
 7505            );
 7506        });
 7507
 7508        let panel = workspace.update_in(cx, ProjectPanel::new);
 7509        cx.run_until_parked();
 7510
 7511        toggle_expand_dir(&panel, "root/dir1", cx);
 7512        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7513        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7514
 7515        assert_eq!(
 7516            visible_entries_as_strings(&panel, 0..20, cx),
 7517            &[
 7518                "v root",
 7519                "    v dir1",
 7520                "        v subdir1/nested1  <== selected",
 7521                "              file1.txt",
 7522                "              file2.txt",
 7523                "        > subdir2",
 7524                "    > dir2/single_file",
 7525            ],
 7526            "Initial state with some dirs expanded"
 7527        );
 7528
 7529        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7530        panel.update(cx, |panel, cx| {
 7531            let project = panel.project.read(cx);
 7532            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7533            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7534        });
 7535
 7536        toggle_expand_dir(&panel, "root/dir1", cx);
 7537
 7538        assert_eq!(
 7539            visible_entries_as_strings(&panel, 0..20, cx),
 7540            &[
 7541                "v root",
 7542                "    v dir1  <== selected",
 7543                "        > subdir1/nested1",
 7544                "        > subdir2",
 7545                "    > dir2/single_file",
 7546            ],
 7547            "Subdirs should be collapsed and folded with auto-fold enabled"
 7548        );
 7549    }
 7550
 7551    // Test 3: With auto-fold disabled
 7552    {
 7553        cx.update(|_, cx| {
 7554            let settings = *ProjectPanelSettings::get_global(cx);
 7555            ProjectPanelSettings::override_global(
 7556                ProjectPanelSettings {
 7557                    auto_fold_dirs: false,
 7558                    ..settings
 7559                },
 7560                cx,
 7561            );
 7562        });
 7563
 7564        let panel = workspace.update_in(cx, ProjectPanel::new);
 7565        cx.run_until_parked();
 7566
 7567        toggle_expand_dir(&panel, "root/dir1", cx);
 7568        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7569        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7570
 7571        assert_eq!(
 7572            visible_entries_as_strings(&panel, 0..20, cx),
 7573            &[
 7574                "v root",
 7575                "    v dir1",
 7576                "        v subdir1",
 7577                "            v nested1  <== selected",
 7578                "                  file1.txt",
 7579                "                  file2.txt",
 7580                "        > subdir2",
 7581                "    > dir2",
 7582            ],
 7583            "Initial state with some dirs expanded and auto-fold disabled"
 7584        );
 7585
 7586        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7587        panel.update(cx, |panel, cx| {
 7588            let project = panel.project.read(cx);
 7589            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7590            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7591        });
 7592
 7593        toggle_expand_dir(&panel, "root/dir1", cx);
 7594
 7595        assert_eq!(
 7596            visible_entries_as_strings(&panel, 0..20, cx),
 7597            &[
 7598                "v root",
 7599                "    v dir1  <== selected",
 7600                "        > subdir1",
 7601                "        > subdir2",
 7602                "    > dir2",
 7603            ],
 7604            "Subdirs should be collapsed but not folded with auto-fold disabled"
 7605        );
 7606    }
 7607}
 7608
 7609#[gpui::test]
 7610async fn test_collapse_selected_entry_and_children_action(cx: &mut gpui::TestAppContext) {
 7611    init_test(cx);
 7612
 7613    let fs = FakeFs::new(cx.executor());
 7614    fs.insert_tree(
 7615        path!("/root"),
 7616        json!({
 7617            "dir1": {
 7618                "subdir1": {
 7619                    "nested1": {
 7620                        "file1.txt": "",
 7621                        "file2.txt": ""
 7622                    },
 7623                },
 7624                "subdir2": {
 7625                    "file3.txt": ""
 7626                }
 7627            },
 7628            "dir2": {
 7629                "file4.txt": ""
 7630            }
 7631        }),
 7632    )
 7633    .await;
 7634
 7635    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7636    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7637    let workspace = window
 7638        .read_with(cx, |mw, _| mw.workspace().clone())
 7639        .unwrap();
 7640    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7641
 7642    let panel = workspace.update_in(cx, ProjectPanel::new);
 7643    cx.run_until_parked();
 7644
 7645    toggle_expand_dir(&panel, "root/dir1", cx);
 7646    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7647    toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7648    toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
 7649    toggle_expand_dir(&panel, "root/dir2", cx);
 7650
 7651    assert_eq!(
 7652        visible_entries_as_strings(&panel, 0..20, cx),
 7653        &[
 7654            "v root",
 7655            "    v dir1",
 7656            "        v subdir1",
 7657            "            v nested1",
 7658            "                  file1.txt",
 7659            "                  file2.txt",
 7660            "        v subdir2",
 7661            "              file3.txt",
 7662            "    v dir2  <== selected",
 7663            "          file4.txt",
 7664        ],
 7665        "Initial state with directories expanded"
 7666    );
 7667
 7668    select_path(&panel, "root/dir1", cx);
 7669    cx.run_until_parked();
 7670
 7671    panel.update_in(cx, |panel, window, cx| {
 7672        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7673    });
 7674    cx.run_until_parked();
 7675
 7676    assert_eq!(
 7677        visible_entries_as_strings(&panel, 0..20, cx),
 7678        &[
 7679            "v root",
 7680            "    > dir1  <== selected",
 7681            "    v dir2",
 7682            "          file4.txt",
 7683        ],
 7684        "dir1 and all its children should be collapsed, dir2 should remain expanded"
 7685    );
 7686
 7687    toggle_expand_dir(&panel, "root/dir1", cx);
 7688    cx.run_until_parked();
 7689
 7690    assert_eq!(
 7691        visible_entries_as_strings(&panel, 0..20, cx),
 7692        &[
 7693            "v root",
 7694            "    v dir1  <== selected",
 7695            "        > subdir1",
 7696            "        > subdir2",
 7697            "    v dir2",
 7698            "          file4.txt",
 7699        ],
 7700        "After re-expanding dir1, its children should still be collapsed"
 7701    );
 7702}
 7703
 7704#[gpui::test]
 7705async fn test_collapse_root_single_worktree(cx: &mut gpui::TestAppContext) {
 7706    init_test(cx);
 7707
 7708    let fs = FakeFs::new(cx.executor());
 7709    fs.insert_tree(
 7710        path!("/root"),
 7711        json!({
 7712            "dir1": {
 7713                "subdir1": {
 7714                    "file1.txt": ""
 7715                },
 7716                "file2.txt": ""
 7717            },
 7718            "dir2": {
 7719                "file3.txt": ""
 7720            }
 7721        }),
 7722    )
 7723    .await;
 7724
 7725    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7726    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7727    let workspace = window
 7728        .read_with(cx, |mw, _| mw.workspace().clone())
 7729        .unwrap();
 7730    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7731
 7732    let panel = workspace.update_in(cx, ProjectPanel::new);
 7733    cx.run_until_parked();
 7734
 7735    toggle_expand_dir(&panel, "root/dir1", cx);
 7736    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7737    toggle_expand_dir(&panel, "root/dir2", cx);
 7738
 7739    assert_eq!(
 7740        visible_entries_as_strings(&panel, 0..20, cx),
 7741        &[
 7742            "v root",
 7743            "    v dir1",
 7744            "        v subdir1",
 7745            "              file1.txt",
 7746            "          file2.txt",
 7747            "    v dir2  <== selected",
 7748            "          file3.txt",
 7749        ],
 7750        "Initial state with directories expanded"
 7751    );
 7752
 7753    // Select the root and collapse it and its children
 7754    select_path(&panel, "root", cx);
 7755    cx.run_until_parked();
 7756
 7757    panel.update_in(cx, |panel, window, cx| {
 7758        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7759    });
 7760    cx.run_until_parked();
 7761
 7762    // The root and all its children should be collapsed
 7763    assert_eq!(
 7764        visible_entries_as_strings(&panel, 0..20, cx),
 7765        &["> root  <== selected"],
 7766        "Root and all children should be collapsed"
 7767    );
 7768
 7769    // Re-expand root and dir1, verify children were recursively collapsed
 7770    toggle_expand_dir(&panel, "root", cx);
 7771    toggle_expand_dir(&panel, "root/dir1", cx);
 7772    cx.run_until_parked();
 7773
 7774    assert_eq!(
 7775        visible_entries_as_strings(&panel, 0..20, cx),
 7776        &[
 7777            "v root",
 7778            "    v dir1  <== selected",
 7779            "        > subdir1",
 7780            "          file2.txt",
 7781            "    > dir2",
 7782        ],
 7783        "After re-expanding root and dir1, subdir1 should still be collapsed"
 7784    );
 7785}
 7786
 7787#[gpui::test]
 7788async fn test_collapse_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 7789    init_test(cx);
 7790
 7791    let fs = FakeFs::new(cx.executor());
 7792    fs.insert_tree(
 7793        path!("/root1"),
 7794        json!({
 7795            "dir1": {
 7796                "subdir1": {
 7797                    "file1.txt": ""
 7798                },
 7799                "file2.txt": ""
 7800            }
 7801        }),
 7802    )
 7803    .await;
 7804    fs.insert_tree(
 7805        path!("/root2"),
 7806        json!({
 7807            "dir2": {
 7808                "file3.txt": ""
 7809            },
 7810            "file4.txt": ""
 7811        }),
 7812    )
 7813    .await;
 7814
 7815    let project = Project::test(
 7816        fs.clone(),
 7817        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 7818        cx,
 7819    )
 7820    .await;
 7821    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7822    let workspace = window
 7823        .read_with(cx, |mw, _| mw.workspace().clone())
 7824        .unwrap();
 7825    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7826
 7827    let panel = workspace.update_in(cx, ProjectPanel::new);
 7828    cx.run_until_parked();
 7829
 7830    toggle_expand_dir(&panel, "root1/dir1", cx);
 7831    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 7832    toggle_expand_dir(&panel, "root2/dir2", cx);
 7833
 7834    assert_eq!(
 7835        visible_entries_as_strings(&panel, 0..20, cx),
 7836        &[
 7837            "v root1",
 7838            "    v dir1",
 7839            "        v subdir1",
 7840            "              file1.txt",
 7841            "          file2.txt",
 7842            "v root2",
 7843            "    v dir2  <== selected",
 7844            "          file3.txt",
 7845            "      file4.txt",
 7846        ],
 7847        "Initial state with directories expanded across worktrees"
 7848    );
 7849
 7850    // Select root1 and collapse it and its children.
 7851    // In a multi-worktree project, this should only collapse the selected worktree,
 7852    // leaving other worktrees unaffected.
 7853    select_path(&panel, "root1", cx);
 7854    cx.run_until_parked();
 7855
 7856    panel.update_in(cx, |panel, window, cx| {
 7857        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7858    });
 7859    cx.run_until_parked();
 7860
 7861    assert_eq!(
 7862        visible_entries_as_strings(&panel, 0..20, cx),
 7863        &[
 7864            "> root1  <== selected",
 7865            "v root2",
 7866            "    v dir2",
 7867            "          file3.txt",
 7868            "      file4.txt",
 7869        ],
 7870        "Only root1 should be collapsed, root2 should remain expanded"
 7871    );
 7872
 7873    // Re-expand root1 and verify its children were recursively collapsed
 7874    toggle_expand_dir(&panel, "root1", cx);
 7875
 7876    assert_eq!(
 7877        visible_entries_as_strings(&panel, 0..20, cx),
 7878        &[
 7879            "v root1  <== selected",
 7880            "    > dir1",
 7881            "v root2",
 7882            "    v dir2",
 7883            "          file3.txt",
 7884            "      file4.txt",
 7885        ],
 7886        "After re-expanding root1, dir1 should still be collapsed, root2 should be unaffected"
 7887    );
 7888}
 7889
 7890#[gpui::test]
 7891async fn test_collapse_non_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 7892    init_test(cx);
 7893
 7894    let fs = FakeFs::new(cx.executor());
 7895    fs.insert_tree(
 7896        path!("/root1"),
 7897        json!({
 7898            "dir1": {
 7899                "subdir1": {
 7900                    "file1.txt": ""
 7901                },
 7902                "file2.txt": ""
 7903            }
 7904        }),
 7905    )
 7906    .await;
 7907    fs.insert_tree(
 7908        path!("/root2"),
 7909        json!({
 7910            "dir2": {
 7911                "subdir2": {
 7912                    "file3.txt": ""
 7913                },
 7914                "file4.txt": ""
 7915            }
 7916        }),
 7917    )
 7918    .await;
 7919
 7920    let project = Project::test(
 7921        fs.clone(),
 7922        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 7923        cx,
 7924    )
 7925    .await;
 7926    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7927    let workspace = window
 7928        .read_with(cx, |mw, _| mw.workspace().clone())
 7929        .unwrap();
 7930    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7931
 7932    let panel = workspace.update_in(cx, ProjectPanel::new);
 7933    cx.run_until_parked();
 7934
 7935    toggle_expand_dir(&panel, "root1/dir1", cx);
 7936    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 7937    toggle_expand_dir(&panel, "root2/dir2", cx);
 7938    toggle_expand_dir(&panel, "root2/dir2/subdir2", cx);
 7939
 7940    assert_eq!(
 7941        visible_entries_as_strings(&panel, 0..20, cx),
 7942        &[
 7943            "v root1",
 7944            "    v dir1",
 7945            "        v subdir1",
 7946            "              file1.txt",
 7947            "          file2.txt",
 7948            "v root2",
 7949            "    v dir2",
 7950            "        v subdir2  <== selected",
 7951            "              file3.txt",
 7952            "          file4.txt",
 7953        ],
 7954        "Initial state with directories expanded across worktrees"
 7955    );
 7956
 7957    // Select dir1 in root1 and collapse it
 7958    select_path(&panel, "root1/dir1", cx);
 7959    cx.run_until_parked();
 7960
 7961    panel.update_in(cx, |panel, window, cx| {
 7962        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7963    });
 7964    cx.run_until_parked();
 7965
 7966    assert_eq!(
 7967        visible_entries_as_strings(&panel, 0..20, cx),
 7968        &[
 7969            "v root1",
 7970            "    > dir1  <== selected",
 7971            "v root2",
 7972            "    v dir2",
 7973            "        v subdir2",
 7974            "              file3.txt",
 7975            "          file4.txt",
 7976        ],
 7977        "Only dir1 should be collapsed, root2 should be completely unaffected"
 7978    );
 7979
 7980    // Re-expand dir1 and verify subdir1 was recursively collapsed
 7981    toggle_expand_dir(&panel, "root1/dir1", cx);
 7982
 7983    assert_eq!(
 7984        visible_entries_as_strings(&panel, 0..20, cx),
 7985        &[
 7986            "v root1",
 7987            "    v dir1  <== selected",
 7988            "        > subdir1",
 7989            "          file2.txt",
 7990            "v root2",
 7991            "    v dir2",
 7992            "        v subdir2",
 7993            "              file3.txt",
 7994            "          file4.txt",
 7995        ],
 7996        "After re-expanding dir1, subdir1 should still be collapsed"
 7997    );
 7998}
 7999
 8000#[gpui::test]
 8001async fn test_collapse_all_for_root_single_worktree(cx: &mut gpui::TestAppContext) {
 8002    init_test(cx);
 8003
 8004    let fs = FakeFs::new(cx.executor());
 8005    fs.insert_tree(
 8006        path!("/root"),
 8007        json!({
 8008            "dir1": {
 8009                "subdir1": {
 8010                    "file1.txt": ""
 8011                },
 8012                "file2.txt": ""
 8013            },
 8014            "dir2": {
 8015                "file3.txt": ""
 8016            }
 8017        }),
 8018    )
 8019    .await;
 8020
 8021    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8022    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8023    let workspace = window
 8024        .read_with(cx, |mw, _| mw.workspace().clone())
 8025        .unwrap();
 8026    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8027
 8028    let panel = workspace.update_in(cx, ProjectPanel::new);
 8029    cx.run_until_parked();
 8030
 8031    toggle_expand_dir(&panel, "root/dir1", cx);
 8032    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 8033    toggle_expand_dir(&panel, "root/dir2", cx);
 8034
 8035    assert_eq!(
 8036        visible_entries_as_strings(&panel, 0..20, cx),
 8037        &[
 8038            "v root",
 8039            "    v dir1",
 8040            "        v subdir1",
 8041            "              file1.txt",
 8042            "          file2.txt",
 8043            "    v dir2  <== selected",
 8044            "          file3.txt",
 8045        ],
 8046        "Initial state with directories expanded"
 8047    );
 8048
 8049    select_path(&panel, "root", cx);
 8050    cx.run_until_parked();
 8051
 8052    panel.update_in(cx, |panel, window, cx| {
 8053        panel.collapse_all_for_root(window, cx);
 8054    });
 8055    cx.run_until_parked();
 8056
 8057    assert_eq!(
 8058        visible_entries_as_strings(&panel, 0..20, cx),
 8059        &["v root  <== selected", "    > dir1", "    > dir2"],
 8060        "Root should remain expanded but all children should be collapsed"
 8061    );
 8062
 8063    toggle_expand_dir(&panel, "root/dir1", cx);
 8064    cx.run_until_parked();
 8065
 8066    assert_eq!(
 8067        visible_entries_as_strings(&panel, 0..20, cx),
 8068        &[
 8069            "v root",
 8070            "    v dir1  <== selected",
 8071            "        > subdir1",
 8072            "          file2.txt",
 8073            "    > dir2",
 8074        ],
 8075        "After re-expanding dir1, subdir1 should still be collapsed"
 8076    );
 8077}
 8078
 8079#[gpui::test]
 8080async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 8081    init_test(cx);
 8082
 8083    let fs = FakeFs::new(cx.executor());
 8084    fs.insert_tree(
 8085        path!("/root1"),
 8086        json!({
 8087            "dir1": {
 8088                "subdir1": {
 8089                    "file1.txt": ""
 8090                },
 8091                "file2.txt": ""
 8092            }
 8093        }),
 8094    )
 8095    .await;
 8096    fs.insert_tree(
 8097        path!("/root2"),
 8098        json!({
 8099            "dir2": {
 8100                "file3.txt": ""
 8101            },
 8102            "file4.txt": ""
 8103        }),
 8104    )
 8105    .await;
 8106
 8107    let project = Project::test(
 8108        fs.clone(),
 8109        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 8110        cx,
 8111    )
 8112    .await;
 8113    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8114    let workspace = window
 8115        .read_with(cx, |mw, _| mw.workspace().clone())
 8116        .unwrap();
 8117    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8118
 8119    let panel = workspace.update_in(cx, ProjectPanel::new);
 8120    cx.run_until_parked();
 8121
 8122    toggle_expand_dir(&panel, "root1/dir1", cx);
 8123    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 8124    toggle_expand_dir(&panel, "root2/dir2", cx);
 8125
 8126    assert_eq!(
 8127        visible_entries_as_strings(&panel, 0..20, cx),
 8128        &[
 8129            "v root1",
 8130            "    v dir1",
 8131            "        v subdir1",
 8132            "              file1.txt",
 8133            "          file2.txt",
 8134            "v root2",
 8135            "    v dir2  <== selected",
 8136            "          file3.txt",
 8137            "      file4.txt",
 8138        ],
 8139        "Initial state with directories expanded across worktrees"
 8140    );
 8141
 8142    select_path(&panel, "root1", cx);
 8143    cx.run_until_parked();
 8144
 8145    panel.update_in(cx, |panel, window, cx| {
 8146        panel.collapse_all_for_root(window, cx);
 8147    });
 8148    cx.run_until_parked();
 8149
 8150    assert_eq!(
 8151        visible_entries_as_strings(&panel, 0..20, cx),
 8152        &[
 8153            "> root1  <== selected",
 8154            "v root2",
 8155            "    v dir2",
 8156            "          file3.txt",
 8157            "      file4.txt",
 8158        ],
 8159        "With multiple worktrees, root1 should collapse completely (including itself)"
 8160    );
 8161}
 8162
 8163#[gpui::test]
 8164async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppContext) {
 8165    init_test(cx);
 8166
 8167    let fs = FakeFs::new(cx.executor());
 8168    fs.insert_tree(
 8169        path!("/root"),
 8170        json!({
 8171            "dir1": {
 8172                "subdir1": {
 8173                    "file1.txt": ""
 8174                },
 8175            },
 8176            "dir2": {
 8177                "file2.txt": ""
 8178            }
 8179        }),
 8180    )
 8181    .await;
 8182
 8183    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8184    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8185    let workspace = window
 8186        .read_with(cx, |mw, _| mw.workspace().clone())
 8187        .unwrap();
 8188    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8189
 8190    let panel = workspace.update_in(cx, ProjectPanel::new);
 8191    cx.run_until_parked();
 8192
 8193    toggle_expand_dir(&panel, "root/dir1", cx);
 8194    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 8195    toggle_expand_dir(&panel, "root/dir2", cx);
 8196
 8197    assert_eq!(
 8198        visible_entries_as_strings(&panel, 0..20, cx),
 8199        &[
 8200            "v root",
 8201            "    v dir1",
 8202            "        v subdir1",
 8203            "              file1.txt",
 8204            "    v dir2  <== selected",
 8205            "          file2.txt",
 8206        ],
 8207        "Initial state with directories expanded"
 8208    );
 8209
 8210    select_path(&panel, "root/dir1", cx);
 8211    cx.run_until_parked();
 8212
 8213    panel.update_in(cx, |panel, window, cx| {
 8214        panel.collapse_all_for_root(window, cx);
 8215    });
 8216    cx.run_until_parked();
 8217
 8218    assert_eq!(
 8219        visible_entries_as_strings(&panel, 0..20, cx),
 8220        &[
 8221            "v root",
 8222            "    v dir1  <== selected",
 8223            "        v subdir1",
 8224            "              file1.txt",
 8225            "    v dir2",
 8226            "          file2.txt",
 8227        ],
 8228        "collapse_all_for_root should be a no-op when called on a non-root directory"
 8229    );
 8230}
 8231
 8232#[gpui::test]
 8233async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
 8234    init_test(cx);
 8235
 8236    let fs = FakeFs::new(cx.executor());
 8237    fs.insert_tree(
 8238        path!("/root"),
 8239        json!({
 8240            "dir1": {
 8241                "file1.txt": "",
 8242            },
 8243        }),
 8244    )
 8245    .await;
 8246
 8247    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8248    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8249    let workspace = window
 8250        .read_with(cx, |mw, _| mw.workspace().clone())
 8251        .unwrap();
 8252    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8253
 8254    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8255        let panel = ProjectPanel::new(workspace, window, cx);
 8256        workspace.add_panel(panel.clone(), window, cx);
 8257        panel
 8258    });
 8259    cx.run_until_parked();
 8260
 8261    #[rustfmt::skip]
 8262    assert_eq!(
 8263        visible_entries_as_strings(&panel, 0..20, cx),
 8264        &[
 8265            "v root",
 8266            "    > dir1",
 8267        ],
 8268        "Initial state with nothing selected"
 8269    );
 8270
 8271    panel.update_in(cx, |panel, window, cx| {
 8272        panel.new_file(&NewFile, window, cx);
 8273    });
 8274    cx.run_until_parked();
 8275    panel.update_in(cx, |panel, window, cx| {
 8276        assert!(panel.filename_editor.read(cx).is_focused(window));
 8277    });
 8278    panel
 8279        .update_in(cx, |panel, window, cx| {
 8280            panel.filename_editor.update(cx, |editor, cx| {
 8281                editor.set_text("hello_from_no_selections", window, cx)
 8282            });
 8283            panel.confirm_edit(true, window, cx).unwrap()
 8284        })
 8285        .await
 8286        .unwrap();
 8287    cx.run_until_parked();
 8288    #[rustfmt::skip]
 8289    assert_eq!(
 8290        visible_entries_as_strings(&panel, 0..20, cx),
 8291        &[
 8292            "v root",
 8293            "    > dir1",
 8294            "      hello_from_no_selections  <== selected  <== marked",
 8295        ],
 8296        "A new file is created under the root directory"
 8297    );
 8298}
 8299
 8300#[gpui::test]
 8301async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
 8302    init_test(cx);
 8303
 8304    let fs = FakeFs::new(cx.executor());
 8305    fs.insert_tree(
 8306        path!("/root"),
 8307        json!({
 8308            "existing_dir": {
 8309                "existing_file.txt": "",
 8310            },
 8311            "existing_file.txt": "",
 8312        }),
 8313    )
 8314    .await;
 8315
 8316    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8317    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8318    let workspace = window
 8319        .read_with(cx, |mw, _| mw.workspace().clone())
 8320        .unwrap();
 8321    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8322
 8323    cx.update(|_, cx| {
 8324        let settings = *ProjectPanelSettings::get_global(cx);
 8325        ProjectPanelSettings::override_global(
 8326            ProjectPanelSettings {
 8327                hide_root: true,
 8328                ..settings
 8329            },
 8330            cx,
 8331        );
 8332    });
 8333
 8334    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8335        let panel = ProjectPanel::new(workspace, window, cx);
 8336        workspace.add_panel(panel.clone(), window, cx);
 8337        panel
 8338    });
 8339    cx.run_until_parked();
 8340
 8341    #[rustfmt::skip]
 8342    assert_eq!(
 8343        visible_entries_as_strings(&panel, 0..20, cx),
 8344        &[
 8345            "> existing_dir",
 8346            "  existing_file.txt",
 8347        ],
 8348        "Initial state with hide_root=true, root should be hidden and nothing selected"
 8349    );
 8350
 8351    panel.update(cx, |panel, _| {
 8352        assert!(
 8353            panel.selection.is_none(),
 8354            "Should have no selection initially"
 8355        );
 8356    });
 8357
 8358    // Test 1: Create new file when no entry is selected
 8359    panel.update_in(cx, |panel, window, cx| {
 8360        panel.new_file(&NewFile, window, cx);
 8361    });
 8362    cx.run_until_parked();
 8363    panel.update_in(cx, |panel, window, cx| {
 8364        assert!(panel.filename_editor.read(cx).is_focused(window));
 8365    });
 8366    cx.run_until_parked();
 8367    #[rustfmt::skip]
 8368    assert_eq!(
 8369        visible_entries_as_strings(&panel, 0..20, cx),
 8370        &[
 8371            "> existing_dir",
 8372            "  [EDITOR: '']  <== selected",
 8373            "  existing_file.txt",
 8374        ],
 8375        "Editor should appear at root level when hide_root=true and no selection"
 8376    );
 8377
 8378    let confirm = panel.update_in(cx, |panel, window, cx| {
 8379        panel.filename_editor.update(cx, |editor, cx| {
 8380            editor.set_text("new_file_at_root.txt", window, cx)
 8381        });
 8382        panel.confirm_edit(true, window, cx).unwrap()
 8383    });
 8384    confirm.await.unwrap();
 8385    cx.run_until_parked();
 8386
 8387    #[rustfmt::skip]
 8388    assert_eq!(
 8389        visible_entries_as_strings(&panel, 0..20, cx),
 8390        &[
 8391            "> existing_dir",
 8392            "  existing_file.txt",
 8393            "  new_file_at_root.txt  <== selected  <== marked",
 8394        ],
 8395        "New file should be created at root level and visible without root prefix"
 8396    );
 8397
 8398    assert!(
 8399        fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
 8400        "File should be created in the actual root directory"
 8401    );
 8402
 8403    // Test 2: Create new directory when no entry is selected
 8404    panel.update(cx, |panel, _| {
 8405        panel.selection = None;
 8406    });
 8407
 8408    panel.update_in(cx, |panel, window, cx| {
 8409        panel.new_directory(&NewDirectory, window, cx);
 8410    });
 8411    cx.run_until_parked();
 8412
 8413    panel.update_in(cx, |panel, window, cx| {
 8414        assert!(panel.filename_editor.read(cx).is_focused(window));
 8415    });
 8416
 8417    #[rustfmt::skip]
 8418    assert_eq!(
 8419        visible_entries_as_strings(&panel, 0..20, cx),
 8420        &[
 8421            "> [EDITOR: '']  <== selected",
 8422            "> existing_dir",
 8423            "  existing_file.txt",
 8424            "  new_file_at_root.txt",
 8425        ],
 8426        "Directory editor should appear at root level when hide_root=true and no selection"
 8427    );
 8428
 8429    let confirm = panel.update_in(cx, |panel, window, cx| {
 8430        panel.filename_editor.update(cx, |editor, cx| {
 8431            editor.set_text("new_dir_at_root", window, cx)
 8432        });
 8433        panel.confirm_edit(true, window, cx).unwrap()
 8434    });
 8435    confirm.await.unwrap();
 8436    cx.run_until_parked();
 8437
 8438    #[rustfmt::skip]
 8439    assert_eq!(
 8440        visible_entries_as_strings(&panel, 0..20, cx),
 8441        &[
 8442            "> existing_dir",
 8443            "v new_dir_at_root  <== selected",
 8444            "  existing_file.txt",
 8445            "  new_file_at_root.txt",
 8446        ],
 8447        "New directory should be created at root level and visible without root prefix"
 8448    );
 8449
 8450    assert!(
 8451        fs.is_dir(Path::new("/root/new_dir_at_root")).await,
 8452        "Directory should be created in the actual root directory"
 8453    );
 8454}
 8455
 8456#[cfg(windows)]
 8457#[gpui::test]
 8458async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) {
 8459    init_test(cx);
 8460
 8461    let fs = FakeFs::new(cx.executor());
 8462    fs.insert_tree(
 8463        path!("/root"),
 8464        json!({
 8465            "dir1": {
 8466                "file1.txt": "",
 8467            },
 8468        }),
 8469    )
 8470    .await;
 8471
 8472    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8473    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8474    let workspace = window
 8475        .read_with(cx, |mw, _| mw.workspace().clone())
 8476        .unwrap();
 8477    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8478
 8479    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8480        let panel = ProjectPanel::new(workspace, window, cx);
 8481        workspace.add_panel(panel.clone(), window, cx);
 8482        panel
 8483    });
 8484    cx.run_until_parked();
 8485
 8486    #[rustfmt::skip]
 8487    assert_eq!(
 8488        visible_entries_as_strings(&panel, 0..20, cx),
 8489        &[
 8490            "v root",
 8491            "    > dir1",
 8492        ],
 8493        "Initial state with nothing selected"
 8494    );
 8495
 8496    panel.update_in(cx, |panel, window, cx| {
 8497        panel.new_file(&NewFile, window, cx);
 8498    });
 8499    cx.run_until_parked();
 8500    panel.update_in(cx, |panel, window, cx| {
 8501        assert!(panel.filename_editor.read(cx).is_focused(window));
 8502    });
 8503    panel
 8504        .update_in(cx, |panel, window, cx| {
 8505            panel
 8506                .filename_editor
 8507                .update(cx, |editor, cx| editor.set_text("foo.", window, cx));
 8508            panel.confirm_edit(true, window, cx).unwrap()
 8509        })
 8510        .await
 8511        .unwrap();
 8512    cx.run_until_parked();
 8513    #[rustfmt::skip]
 8514    assert_eq!(
 8515        visible_entries_as_strings(&panel, 0..20, cx),
 8516        &[
 8517            "v root",
 8518            "    > dir1",
 8519            "      foo  <== selected  <== marked",
 8520        ],
 8521        "A new file is created under the root directory without the trailing dot"
 8522    );
 8523}
 8524
 8525#[gpui::test]
 8526async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
 8527    init_test(cx);
 8528
 8529    let fs = FakeFs::new(cx.executor());
 8530    fs.insert_tree(
 8531        "/root",
 8532        json!({
 8533            "dir1": {
 8534                "file1.txt": "",
 8535                "dir2": {
 8536                    "file2.txt": ""
 8537                }
 8538            },
 8539            "file3.txt": ""
 8540        }),
 8541    )
 8542    .await;
 8543
 8544    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 8545    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8546    let workspace = window
 8547        .read_with(cx, |mw, _| mw.workspace().clone())
 8548        .unwrap();
 8549    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8550    let panel = workspace.update_in(cx, ProjectPanel::new);
 8551    cx.run_until_parked();
 8552
 8553    panel.update(cx, |panel, cx| {
 8554        let project = panel.project.read(cx);
 8555        let worktree = project.visible_worktrees(cx).next().unwrap();
 8556        let worktree = worktree.read(cx);
 8557
 8558        // Test 1: Target is a directory, should highlight the directory itself
 8559        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
 8560        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
 8561        assert_eq!(
 8562            result,
 8563            Some(dir_entry.id),
 8564            "Should highlight directory itself"
 8565        );
 8566
 8567        // Test 2: Target is nested file, should highlight immediate parent
 8568        let nested_file = worktree
 8569            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
 8570            .unwrap();
 8571        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
 8572        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
 8573        assert_eq!(
 8574            result,
 8575            Some(nested_parent.id),
 8576            "Should highlight immediate parent"
 8577        );
 8578
 8579        // Test 3: Target is root level file, should highlight root
 8580        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
 8581        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
 8582        assert_eq!(
 8583            result,
 8584            Some(worktree.root_entry().unwrap().id),
 8585            "Root level file should return None"
 8586        );
 8587
 8588        // Test 4: Target is root itself, should highlight root
 8589        let root_entry = worktree.root_entry().unwrap();
 8590        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
 8591        assert_eq!(
 8592            result,
 8593            Some(root_entry.id),
 8594            "Root level file should return None"
 8595        );
 8596    });
 8597}
 8598
 8599#[gpui::test]
 8600async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
 8601    init_test(cx);
 8602
 8603    let fs = FakeFs::new(cx.executor());
 8604    fs.insert_tree(
 8605        "/root",
 8606        json!({
 8607            "parent_dir": {
 8608                "child_file.txt": "",
 8609                "sibling_file.txt": "",
 8610                "child_dir": {
 8611                    "nested_file.txt": ""
 8612                }
 8613            },
 8614            "other_dir": {
 8615                "other_file.txt": ""
 8616            }
 8617        }),
 8618    )
 8619    .await;
 8620
 8621    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 8622    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8623    let workspace = window
 8624        .read_with(cx, |mw, _| mw.workspace().clone())
 8625        .unwrap();
 8626    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8627    let panel = workspace.update_in(cx, ProjectPanel::new);
 8628    cx.run_until_parked();
 8629
 8630    panel.update(cx, |panel, cx| {
 8631        let project = panel.project.read(cx);
 8632        let worktree = project.visible_worktrees(cx).next().unwrap();
 8633        let worktree_id = worktree.read(cx).id();
 8634        let worktree = worktree.read(cx);
 8635
 8636        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
 8637        let child_file = worktree
 8638            .entry_for_path(rel_path("parent_dir/child_file.txt"))
 8639            .unwrap();
 8640        let sibling_file = worktree
 8641            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
 8642            .unwrap();
 8643        let child_dir = worktree
 8644            .entry_for_path(rel_path("parent_dir/child_dir"))
 8645            .unwrap();
 8646        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
 8647        let other_file = worktree
 8648            .entry_for_path(rel_path("other_dir/other_file.txt"))
 8649            .unwrap();
 8650
 8651        // Test 1: Single item drag, don't highlight parent directory
 8652        let dragged_selection = DraggedSelection {
 8653            active_selection: SelectedEntry {
 8654                worktree_id,
 8655                entry_id: child_file.id,
 8656            },
 8657            marked_selections: Arc::new([SelectedEntry {
 8658                worktree_id,
 8659                entry_id: child_file.id,
 8660            }]),
 8661        };
 8662        let result =
 8663            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
 8664        assert_eq!(result, None, "Should not highlight parent of dragged item");
 8665
 8666        // Test 2: Single item drag, don't highlight sibling files
 8667        let result = panel.highlight_entry_for_selection_drag(
 8668            sibling_file,
 8669            worktree,
 8670            &dragged_selection,
 8671            cx,
 8672        );
 8673        assert_eq!(result, None, "Should not highlight sibling files");
 8674
 8675        // Test 3: Single item drag, highlight unrelated directory
 8676        let result =
 8677            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
 8678        assert_eq!(
 8679            result,
 8680            Some(other_dir.id),
 8681            "Should highlight unrelated directory"
 8682        );
 8683
 8684        // Test 4: Single item drag, highlight sibling directory
 8685        let result =
 8686            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
 8687        assert_eq!(
 8688            result,
 8689            Some(child_dir.id),
 8690            "Should highlight sibling directory"
 8691        );
 8692
 8693        // Test 5: Multiple items drag, highlight parent directory
 8694        let dragged_selection = DraggedSelection {
 8695            active_selection: SelectedEntry {
 8696                worktree_id,
 8697                entry_id: child_file.id,
 8698            },
 8699            marked_selections: Arc::new([
 8700                SelectedEntry {
 8701                    worktree_id,
 8702                    entry_id: child_file.id,
 8703                },
 8704                SelectedEntry {
 8705                    worktree_id,
 8706                    entry_id: sibling_file.id,
 8707                },
 8708            ]),
 8709        };
 8710        let result =
 8711            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
 8712        assert_eq!(
 8713            result,
 8714            Some(parent_dir.id),
 8715            "Should highlight parent with multiple items"
 8716        );
 8717
 8718        // Test 6: Target is file in different directory, highlight parent
 8719        let result =
 8720            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
 8721        assert_eq!(
 8722            result,
 8723            Some(other_dir.id),
 8724            "Should highlight parent of target file"
 8725        );
 8726
 8727        // Test 7: Target is directory, always highlight
 8728        let result =
 8729            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
 8730        assert_eq!(
 8731            result,
 8732            Some(child_dir.id),
 8733            "Should always highlight directories"
 8734        );
 8735    });
 8736}
 8737
 8738#[gpui::test]
 8739async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
 8740    init_test(cx);
 8741
 8742    let fs = FakeFs::new(cx.executor());
 8743    fs.insert_tree(
 8744        "/root1",
 8745        json!({
 8746            "src": {
 8747                "main.rs": "",
 8748                "lib.rs": ""
 8749            }
 8750        }),
 8751    )
 8752    .await;
 8753    fs.insert_tree(
 8754        "/root2",
 8755        json!({
 8756            "src": {
 8757                "main.rs": "",
 8758                "test.rs": ""
 8759            }
 8760        }),
 8761    )
 8762    .await;
 8763
 8764    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 8765    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8766    let workspace = window
 8767        .read_with(cx, |mw, _| mw.workspace().clone())
 8768        .unwrap();
 8769    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8770    let panel = workspace.update_in(cx, ProjectPanel::new);
 8771    cx.run_until_parked();
 8772
 8773    panel.update(cx, |panel, cx| {
 8774        let project = panel.project.read(cx);
 8775        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 8776
 8777        let worktree_a = &worktrees[0];
 8778        let main_rs_from_a = worktree_a
 8779            .read(cx)
 8780            .entry_for_path(rel_path("src/main.rs"))
 8781            .unwrap();
 8782
 8783        let worktree_b = &worktrees[1];
 8784        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
 8785        let main_rs_from_b = worktree_b
 8786            .read(cx)
 8787            .entry_for_path(rel_path("src/main.rs"))
 8788            .unwrap();
 8789
 8790        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
 8791        let dragged_selection = DraggedSelection {
 8792            active_selection: SelectedEntry {
 8793                worktree_id: worktree_a.read(cx).id(),
 8794                entry_id: main_rs_from_a.id,
 8795            },
 8796            marked_selections: Arc::new([SelectedEntry {
 8797                worktree_id: worktree_a.read(cx).id(),
 8798                entry_id: main_rs_from_a.id,
 8799            }]),
 8800        };
 8801
 8802        let result = panel.highlight_entry_for_selection_drag(
 8803            src_dir_from_b,
 8804            worktree_b.read(cx),
 8805            &dragged_selection,
 8806            cx,
 8807        );
 8808        assert_eq!(
 8809            result,
 8810            Some(src_dir_from_b.id),
 8811            "Should highlight target directory from different worktree even with same relative path"
 8812        );
 8813
 8814        // Test dragging file from worktree A onto file with same relative path in worktree B
 8815        let result = panel.highlight_entry_for_selection_drag(
 8816            main_rs_from_b,
 8817            worktree_b.read(cx),
 8818            &dragged_selection,
 8819            cx,
 8820        );
 8821        assert_eq!(
 8822            result,
 8823            Some(src_dir_from_b.id),
 8824            "Should highlight parent of target file from different worktree"
 8825        );
 8826    });
 8827}
 8828
 8829#[gpui::test]
 8830async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
 8831    init_test(cx);
 8832
 8833    let fs = FakeFs::new(cx.executor());
 8834    fs.insert_tree(
 8835        "/root1",
 8836        json!({
 8837            "parent_dir": {
 8838                "child_file.txt": "",
 8839                "nested_dir": {
 8840                    "nested_file.txt": ""
 8841                }
 8842            },
 8843            "root_file.txt": ""
 8844        }),
 8845    )
 8846    .await;
 8847
 8848    fs.insert_tree(
 8849        "/root2",
 8850        json!({
 8851            "other_dir": {
 8852                "other_file.txt": ""
 8853            }
 8854        }),
 8855    )
 8856    .await;
 8857
 8858    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 8859    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8860    let workspace = window
 8861        .read_with(cx, |mw, _| mw.workspace().clone())
 8862        .unwrap();
 8863    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8864    let panel = workspace.update_in(cx, ProjectPanel::new);
 8865    cx.run_until_parked();
 8866
 8867    panel.update(cx, |panel, cx| {
 8868        let project = panel.project.read(cx);
 8869        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 8870        let worktree1 = worktrees[0].read(cx);
 8871        let worktree2 = worktrees[1].read(cx);
 8872        let worktree1_id = worktree1.id();
 8873        let _worktree2_id = worktree2.id();
 8874
 8875        let root1_entry = worktree1.root_entry().unwrap();
 8876        let root2_entry = worktree2.root_entry().unwrap();
 8877        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
 8878        let child_file = worktree1
 8879            .entry_for_path(rel_path("parent_dir/child_file.txt"))
 8880            .unwrap();
 8881        let nested_file = worktree1
 8882            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
 8883            .unwrap();
 8884        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
 8885
 8886        // Test 1: Multiple entries - should always highlight background
 8887        let multiple_dragged_selection = DraggedSelection {
 8888            active_selection: SelectedEntry {
 8889                worktree_id: worktree1_id,
 8890                entry_id: child_file.id,
 8891            },
 8892            marked_selections: Arc::new([
 8893                SelectedEntry {
 8894                    worktree_id: worktree1_id,
 8895                    entry_id: child_file.id,
 8896                },
 8897                SelectedEntry {
 8898                    worktree_id: worktree1_id,
 8899                    entry_id: nested_file.id,
 8900                },
 8901            ]),
 8902        };
 8903
 8904        let result = panel.should_highlight_background_for_selection_drag(
 8905            &multiple_dragged_selection,
 8906            root1_entry.id,
 8907            cx,
 8908        );
 8909        assert!(result, "Should highlight background for multiple entries");
 8910
 8911        // Test 2: Single entry with non-empty parent path - should highlight background
 8912        let nested_dragged_selection = DraggedSelection {
 8913            active_selection: SelectedEntry {
 8914                worktree_id: worktree1_id,
 8915                entry_id: nested_file.id,
 8916            },
 8917            marked_selections: Arc::new([SelectedEntry {
 8918                worktree_id: worktree1_id,
 8919                entry_id: nested_file.id,
 8920            }]),
 8921        };
 8922
 8923        let result = panel.should_highlight_background_for_selection_drag(
 8924            &nested_dragged_selection,
 8925            root1_entry.id,
 8926            cx,
 8927        );
 8928        assert!(result, "Should highlight background for nested file");
 8929
 8930        // Test 3: Single entry at root level, same worktree - should NOT highlight background
 8931        let root_file_dragged_selection = DraggedSelection {
 8932            active_selection: SelectedEntry {
 8933                worktree_id: worktree1_id,
 8934                entry_id: root_file.id,
 8935            },
 8936            marked_selections: Arc::new([SelectedEntry {
 8937                worktree_id: worktree1_id,
 8938                entry_id: root_file.id,
 8939            }]),
 8940        };
 8941
 8942        let result = panel.should_highlight_background_for_selection_drag(
 8943            &root_file_dragged_selection,
 8944            root1_entry.id,
 8945            cx,
 8946        );
 8947        assert!(
 8948            !result,
 8949            "Should NOT highlight background for root file in same worktree"
 8950        );
 8951
 8952        // Test 4: Single entry at root level, different worktree - should highlight background
 8953        let result = panel.should_highlight_background_for_selection_drag(
 8954            &root_file_dragged_selection,
 8955            root2_entry.id,
 8956            cx,
 8957        );
 8958        assert!(
 8959            result,
 8960            "Should highlight background for root file from different worktree"
 8961        );
 8962
 8963        // Test 5: Single entry in subdirectory - should highlight background
 8964        let child_file_dragged_selection = DraggedSelection {
 8965            active_selection: SelectedEntry {
 8966                worktree_id: worktree1_id,
 8967                entry_id: child_file.id,
 8968            },
 8969            marked_selections: Arc::new([SelectedEntry {
 8970                worktree_id: worktree1_id,
 8971                entry_id: child_file.id,
 8972            }]),
 8973        };
 8974
 8975        let result = panel.should_highlight_background_for_selection_drag(
 8976            &child_file_dragged_selection,
 8977            root1_entry.id,
 8978            cx,
 8979        );
 8980        assert!(
 8981            result,
 8982            "Should highlight background for file with non-empty parent path"
 8983        );
 8984    });
 8985}
 8986
 8987#[gpui::test]
 8988async fn test_hide_root(cx: &mut gpui::TestAppContext) {
 8989    init_test(cx);
 8990
 8991    let fs = FakeFs::new(cx.executor());
 8992    fs.insert_tree(
 8993        "/root1",
 8994        json!({
 8995            "dir1": {
 8996                "file1.txt": "content",
 8997                "file2.txt": "content",
 8998            },
 8999            "dir2": {
 9000                "file3.txt": "content",
 9001            },
 9002            "file4.txt": "content",
 9003        }),
 9004    )
 9005    .await;
 9006
 9007    fs.insert_tree(
 9008        "/root2",
 9009        json!({
 9010            "dir3": {
 9011                "file5.txt": "content",
 9012            },
 9013            "file6.txt": "content",
 9014        }),
 9015    )
 9016    .await;
 9017
 9018    // Test 1: Single worktree with hide_root = false
 9019    {
 9020        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 9021        let window =
 9022            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9023        let workspace = window
 9024            .read_with(cx, |mw, _| mw.workspace().clone())
 9025            .unwrap();
 9026        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9027
 9028        cx.update(|_, cx| {
 9029            let settings = *ProjectPanelSettings::get_global(cx);
 9030            ProjectPanelSettings::override_global(
 9031                ProjectPanelSettings {
 9032                    hide_root: false,
 9033                    ..settings
 9034                },
 9035                cx,
 9036            );
 9037        });
 9038
 9039        let panel = workspace.update_in(cx, ProjectPanel::new);
 9040        cx.run_until_parked();
 9041
 9042        #[rustfmt::skip]
 9043        assert_eq!(
 9044            visible_entries_as_strings(&panel, 0..10, cx),
 9045            &[
 9046                "v root1",
 9047                "    > dir1",
 9048                "    > dir2",
 9049                "      file4.txt",
 9050            ],
 9051            "With hide_root=false and single worktree, root should be visible"
 9052        );
 9053    }
 9054
 9055    // Test 2: Single worktree with hide_root = true
 9056    {
 9057        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 9058        let window =
 9059            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9060        let workspace = window
 9061            .read_with(cx, |mw, _| mw.workspace().clone())
 9062            .unwrap();
 9063        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9064
 9065        // Set hide_root to true
 9066        cx.update(|_, cx| {
 9067            let settings = *ProjectPanelSettings::get_global(cx);
 9068            ProjectPanelSettings::override_global(
 9069                ProjectPanelSettings {
 9070                    hide_root: true,
 9071                    ..settings
 9072                },
 9073                cx,
 9074            );
 9075        });
 9076
 9077        let panel = workspace.update_in(cx, ProjectPanel::new);
 9078        cx.run_until_parked();
 9079
 9080        assert_eq!(
 9081            visible_entries_as_strings(&panel, 0..10, cx),
 9082            &["> dir1", "> dir2", "  file4.txt",],
 9083            "With hide_root=true and single worktree, root should be hidden"
 9084        );
 9085
 9086        // Test expanding directories still works without root
 9087        toggle_expand_dir(&panel, "root1/dir1", cx);
 9088        assert_eq!(
 9089            visible_entries_as_strings(&panel, 0..10, cx),
 9090            &[
 9091                "v dir1  <== selected",
 9092                "      file1.txt",
 9093                "      file2.txt",
 9094                "> dir2",
 9095                "  file4.txt",
 9096            ],
 9097            "Should be able to expand directories even when root is hidden"
 9098        );
 9099    }
 9100
 9101    // Test 3: Multiple worktrees with hide_root = true
 9102    {
 9103        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 9104        let window =
 9105            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9106        let workspace = window
 9107            .read_with(cx, |mw, _| mw.workspace().clone())
 9108            .unwrap();
 9109        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9110
 9111        // Set hide_root to true
 9112        cx.update(|_, cx| {
 9113            let settings = *ProjectPanelSettings::get_global(cx);
 9114            ProjectPanelSettings::override_global(
 9115                ProjectPanelSettings {
 9116                    hide_root: true,
 9117                    ..settings
 9118                },
 9119                cx,
 9120            );
 9121        });
 9122
 9123        let panel = workspace.update_in(cx, ProjectPanel::new);
 9124        cx.run_until_parked();
 9125
 9126        assert_eq!(
 9127            visible_entries_as_strings(&panel, 0..10, cx),
 9128            &[
 9129                "v root1",
 9130                "    > dir1",
 9131                "    > dir2",
 9132                "      file4.txt",
 9133                "v root2",
 9134                "    > dir3",
 9135                "      file6.txt",
 9136            ],
 9137            "With hide_root=true and multiple worktrees, roots should still be visible"
 9138        );
 9139    }
 9140
 9141    // Test 4: Multiple worktrees with hide_root = false
 9142    {
 9143        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 9144        let window =
 9145            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9146        let workspace = window
 9147            .read_with(cx, |mw, _| mw.workspace().clone())
 9148            .unwrap();
 9149        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9150
 9151        cx.update(|_, cx| {
 9152            let settings = *ProjectPanelSettings::get_global(cx);
 9153            ProjectPanelSettings::override_global(
 9154                ProjectPanelSettings {
 9155                    hide_root: false,
 9156                    ..settings
 9157                },
 9158                cx,
 9159            );
 9160        });
 9161
 9162        let panel = workspace.update_in(cx, ProjectPanel::new);
 9163        cx.run_until_parked();
 9164
 9165        assert_eq!(
 9166            visible_entries_as_strings(&panel, 0..10, cx),
 9167            &[
 9168                "v root1",
 9169                "    > dir1",
 9170                "    > dir2",
 9171                "      file4.txt",
 9172                "v root2",
 9173                "    > dir3",
 9174                "      file6.txt",
 9175            ],
 9176            "With hide_root=false and multiple worktrees, roots should be visible"
 9177        );
 9178    }
 9179}
 9180
 9181#[gpui::test]
 9182async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
 9183    init_test_with_editor(cx);
 9184
 9185    let fs = FakeFs::new(cx.executor());
 9186    fs.insert_tree(
 9187        "/root",
 9188        json!({
 9189            "file1.txt": "content of file1",
 9190            "file2.txt": "content of file2",
 9191            "dir1": {
 9192                "file3.txt": "content of file3"
 9193            }
 9194        }),
 9195    )
 9196    .await;
 9197
 9198    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9199    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9200    let workspace = window
 9201        .read_with(cx, |mw, _| mw.workspace().clone())
 9202        .unwrap();
 9203    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9204    let panel = workspace.update_in(cx, ProjectPanel::new);
 9205    cx.run_until_parked();
 9206
 9207    let file1_path = "root/file1.txt";
 9208    let file2_path = "root/file2.txt";
 9209    select_path_with_mark(&panel, file1_path, cx);
 9210    select_path_with_mark(&panel, file2_path, cx);
 9211
 9212    panel.update_in(cx, |panel, window, cx| {
 9213        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
 9214    });
 9215    cx.executor().run_until_parked();
 9216
 9217    workspace.update_in(cx, |workspace, _, cx| {
 9218        let active_items = workspace
 9219            .panes()
 9220            .iter()
 9221            .filter_map(|pane| pane.read(cx).active_item())
 9222            .collect::<Vec<_>>();
 9223        assert_eq!(active_items.len(), 1);
 9224        let diff_view = active_items
 9225            .into_iter()
 9226            .next()
 9227            .unwrap()
 9228            .downcast::<FileDiffView>()
 9229            .expect("Open item should be an FileDiffView");
 9230        assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
 9231        assert_eq!(
 9232            diff_view.tab_tooltip_text(cx).unwrap(),
 9233            format!(
 9234                "{}{}",
 9235                rel_path(file1_path).display(PathStyle::local()),
 9236                rel_path(file2_path).display(PathStyle::local())
 9237            )
 9238        );
 9239    });
 9240
 9241    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
 9242    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
 9243    let worktree_id = panel.update(cx, |panel, cx| {
 9244        panel
 9245            .project
 9246            .read(cx)
 9247            .worktrees(cx)
 9248            .next()
 9249            .unwrap()
 9250            .read(cx)
 9251            .id()
 9252    });
 9253
 9254    let expected_entries = [
 9255        SelectedEntry {
 9256            worktree_id,
 9257            entry_id: file1_entry_id,
 9258        },
 9259        SelectedEntry {
 9260            worktree_id,
 9261            entry_id: file2_entry_id,
 9262        },
 9263    ];
 9264    panel.update(cx, |panel, _cx| {
 9265        assert_eq!(
 9266            &panel.marked_entries, &expected_entries,
 9267            "Should keep marked entries after comparison"
 9268        );
 9269    });
 9270
 9271    panel.update(cx, |panel, cx| {
 9272        panel.project.update(cx, |_, cx| {
 9273            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
 9274        })
 9275    });
 9276
 9277    panel.update(cx, |panel, _cx| {
 9278        assert_eq!(
 9279            &panel.marked_entries, &expected_entries,
 9280            "Marked entries should persist after focusing back on the project panel"
 9281        );
 9282    });
 9283}
 9284
 9285#[gpui::test]
 9286async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
 9287    init_test_with_editor(cx);
 9288
 9289    let fs = FakeFs::new(cx.executor());
 9290    fs.insert_tree(
 9291        "/root",
 9292        json!({
 9293            "file1.txt": "content of file1",
 9294            "file2.txt": "content of file2",
 9295            "dir1": {},
 9296            "dir2": {
 9297                "file3.txt": "content of file3"
 9298            }
 9299        }),
 9300    )
 9301    .await;
 9302
 9303    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9304    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9305    let workspace = window
 9306        .read_with(cx, |mw, _| mw.workspace().clone())
 9307        .unwrap();
 9308    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9309    let panel = workspace.update_in(cx, ProjectPanel::new);
 9310    cx.run_until_parked();
 9311
 9312    // Test 1: When only one file is selected, there should be no compare option
 9313    select_path(&panel, "root/file1.txt", cx);
 9314
 9315    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9316    assert_eq!(
 9317        selected_files, None,
 9318        "Should not have compare option when only one file is selected"
 9319    );
 9320
 9321    // Test 2: When multiple files are selected, there should be a compare option
 9322    select_path_with_mark(&panel, "root/file1.txt", cx);
 9323    select_path_with_mark(&panel, "root/file2.txt", cx);
 9324
 9325    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9326    assert!(
 9327        selected_files.is_some(),
 9328        "Should have files selected for comparison"
 9329    );
 9330    if let Some((file1, file2)) = selected_files {
 9331        assert!(
 9332            file1.to_string_lossy().ends_with("file1.txt")
 9333                && file2.to_string_lossy().ends_with("file2.txt"),
 9334            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
 9335        );
 9336    }
 9337
 9338    // Test 3: Selecting a directory shouldn't count as a comparable file
 9339    select_path_with_mark(&panel, "root/dir1", cx);
 9340
 9341    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9342    assert!(
 9343        selected_files.is_some(),
 9344        "Directory selection should not affect comparable files"
 9345    );
 9346    if let Some((file1, file2)) = selected_files {
 9347        assert!(
 9348            file1.to_string_lossy().ends_with("file1.txt")
 9349                && file2.to_string_lossy().ends_with("file2.txt"),
 9350            "Selecting a directory should not affect the number of comparable files"
 9351        );
 9352    }
 9353
 9354    // Test 4: Selecting one more file
 9355    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
 9356
 9357    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9358    assert!(
 9359        selected_files.is_some(),
 9360        "Directory selection should not affect comparable files"
 9361    );
 9362    if let Some((file1, file2)) = selected_files {
 9363        assert!(
 9364            file1.to_string_lossy().ends_with("file2.txt")
 9365                && file2.to_string_lossy().ends_with("file3.txt"),
 9366            "Selecting a directory should not affect the number of comparable files"
 9367        );
 9368    }
 9369}
 9370
 9371#[gpui::test]
 9372async fn test_reveal_in_file_manager_path_falls_back_to_worktree_root(
 9373    cx: &mut gpui::TestAppContext,
 9374) {
 9375    init_test(cx);
 9376
 9377    let fs = FakeFs::new(cx.executor());
 9378    fs.insert_tree(
 9379        "/root",
 9380        json!({
 9381            "file.txt": "content",
 9382            "dir": {},
 9383        }),
 9384    )
 9385    .await;
 9386
 9387    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9388    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9389    let workspace = window
 9390        .read_with(cx, |mw, _| mw.workspace().clone())
 9391        .unwrap();
 9392    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9393    let panel = workspace.update_in(cx, ProjectPanel::new);
 9394    cx.run_until_parked();
 9395
 9396    select_path(&panel, "root/file.txt", cx);
 9397    let selected_reveal_path = panel
 9398        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
 9399        .expect("selected entry should produce a reveal path");
 9400    assert!(
 9401        selected_reveal_path.ends_with(Path::new("file.txt")),
 9402        "Expected selected file path, got {:?}",
 9403        selected_reveal_path
 9404    );
 9405
 9406    panel.update(cx, |panel, _| {
 9407        panel.selection = None;
 9408        panel.marked_entries.clear();
 9409    });
 9410    let fallback_reveal_path = panel
 9411        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
 9412        .expect("project root should be used when selection is empty");
 9413    assert!(
 9414        fallback_reveal_path.ends_with(Path::new("root")),
 9415        "Expected worktree root path, got {:?}",
 9416        fallback_reveal_path
 9417    );
 9418}
 9419
 9420#[gpui::test]
 9421async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
 9422    init_test(cx);
 9423
 9424    let fs = FakeFs::new(cx.executor());
 9425    fs.insert_tree(
 9426        "/root",
 9427        json!({
 9428            ".hidden-file.txt": "hidden file content",
 9429            "visible-file.txt": "visible file content",
 9430            ".hidden-parent-dir": {
 9431                "nested-dir": {
 9432                    "file.txt": "file content",
 9433                }
 9434            },
 9435            "visible-dir": {
 9436                "file-in-visible.txt": "file content",
 9437                "nested": {
 9438                    ".hidden-nested-dir": {
 9439                        ".double-hidden-dir": {
 9440                            "deep-file-1.txt": "deep content 1",
 9441                            "deep-file-2.txt": "deep content 2"
 9442                        },
 9443                        "hidden-nested-file-1.txt": "hidden nested 1",
 9444                        "hidden-nested-file-2.txt": "hidden nested 2"
 9445                    },
 9446                    "visible-nested-file.txt": "visible nested content"
 9447                }
 9448            }
 9449        }),
 9450    )
 9451    .await;
 9452
 9453    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9454    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9455    let workspace = window
 9456        .read_with(cx, |mw, _| mw.workspace().clone())
 9457        .unwrap();
 9458    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9459
 9460    cx.update(|_, cx| {
 9461        let settings = *ProjectPanelSettings::get_global(cx);
 9462        ProjectPanelSettings::override_global(
 9463            ProjectPanelSettings {
 9464                hide_hidden: false,
 9465                ..settings
 9466            },
 9467            cx,
 9468        );
 9469    });
 9470
 9471    let panel = workspace.update_in(cx, ProjectPanel::new);
 9472    cx.run_until_parked();
 9473
 9474    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
 9475    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
 9476    toggle_expand_dir(&panel, "root/visible-dir", cx);
 9477    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
 9478    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
 9479    toggle_expand_dir(
 9480        &panel,
 9481        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
 9482        cx,
 9483    );
 9484
 9485    let expanded = [
 9486        "v root",
 9487        "    v .hidden-parent-dir",
 9488        "        v nested-dir",
 9489        "              file.txt",
 9490        "    v visible-dir",
 9491        "        v nested",
 9492        "            v .hidden-nested-dir",
 9493        "                v .double-hidden-dir  <== selected",
 9494        "                      deep-file-1.txt",
 9495        "                      deep-file-2.txt",
 9496        "                  hidden-nested-file-1.txt",
 9497        "                  hidden-nested-file-2.txt",
 9498        "              visible-nested-file.txt",
 9499        "          file-in-visible.txt",
 9500        "      .hidden-file.txt",
 9501        "      visible-file.txt",
 9502    ];
 9503
 9504    assert_eq!(
 9505        visible_entries_as_strings(&panel, 0..30, cx),
 9506        &expanded,
 9507        "With hide_hidden=false, contents of hidden nested directory should be visible"
 9508    );
 9509
 9510    cx.update(|_, cx| {
 9511        let settings = *ProjectPanelSettings::get_global(cx);
 9512        ProjectPanelSettings::override_global(
 9513            ProjectPanelSettings {
 9514                hide_hidden: true,
 9515                ..settings
 9516            },
 9517            cx,
 9518        );
 9519    });
 9520
 9521    panel.update_in(cx, |panel, window, cx| {
 9522        panel.update_visible_entries(None, false, false, window, cx);
 9523    });
 9524    cx.run_until_parked();
 9525
 9526    assert_eq!(
 9527        visible_entries_as_strings(&panel, 0..30, cx),
 9528        &[
 9529            "v root",
 9530            "    v visible-dir",
 9531            "        v nested",
 9532            "              visible-nested-file.txt",
 9533            "          file-in-visible.txt",
 9534            "      visible-file.txt",
 9535        ],
 9536        "With hide_hidden=false, contents of hidden nested directory should be visible"
 9537    );
 9538
 9539    panel.update_in(cx, |panel, window, cx| {
 9540        let settings = *ProjectPanelSettings::get_global(cx);
 9541        ProjectPanelSettings::override_global(
 9542            ProjectPanelSettings {
 9543                hide_hidden: false,
 9544                ..settings
 9545            },
 9546            cx,
 9547        );
 9548        panel.update_visible_entries(None, false, false, window, cx);
 9549    });
 9550    cx.run_until_parked();
 9551
 9552    assert_eq!(
 9553        visible_entries_as_strings(&panel, 0..30, cx),
 9554        &expanded,
 9555        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
 9556    );
 9557}
 9558
 9559fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 9560    let path = rel_path(path);
 9561    panel.update_in(cx, |panel, window, cx| {
 9562        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9563            let worktree = worktree.read(cx);
 9564            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9565                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 9566                panel.update_visible_entries(
 9567                    Some((worktree.id(), entry_id)),
 9568                    false,
 9569                    false,
 9570                    window,
 9571                    cx,
 9572                );
 9573                return;
 9574            }
 9575        }
 9576        panic!("no worktree for path {:?}", path);
 9577    });
 9578    cx.run_until_parked();
 9579}
 9580
 9581fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 9582    let path = rel_path(path);
 9583    panel.update(cx, |panel, cx| {
 9584        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9585            let worktree = worktree.read(cx);
 9586            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9587                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 9588                let entry = crate::SelectedEntry {
 9589                    worktree_id: worktree.id(),
 9590                    entry_id,
 9591                };
 9592                if !panel.marked_entries.contains(&entry) {
 9593                    panel.marked_entries.push(entry);
 9594                }
 9595                panel.selection = Some(entry);
 9596                return;
 9597            }
 9598        }
 9599        panic!("no worktree for path {:?}", path);
 9600    });
 9601}
 9602
 9603/// `leaf_path` is the full path to the leaf entry (e.g., "root/a/b/c")
 9604/// `active_ancestor_path` is the path to the folded component that should be active.
 9605fn select_folded_path_with_mark(
 9606    panel: &Entity<ProjectPanel>,
 9607    leaf_path: &str,
 9608    active_ancestor_path: &str,
 9609    cx: &mut VisualTestContext,
 9610) {
 9611    select_path_with_mark(panel, leaf_path, cx);
 9612    set_folded_active_ancestor(panel, leaf_path, active_ancestor_path, cx);
 9613}
 9614
 9615fn set_folded_active_ancestor(
 9616    panel: &Entity<ProjectPanel>,
 9617    leaf_path: &str,
 9618    active_ancestor_path: &str,
 9619    cx: &mut VisualTestContext,
 9620) {
 9621    let leaf_path = rel_path(leaf_path);
 9622    let active_ancestor_path = rel_path(active_ancestor_path);
 9623    panel.update(cx, |panel, cx| {
 9624        let mut leaf_entry_id = None;
 9625        let mut target_entry_id = None;
 9626
 9627        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9628            let worktree = worktree.read(cx);
 9629            if let Ok(relative_path) = leaf_path.strip_prefix(worktree.root_name()) {
 9630                leaf_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9631            }
 9632            if let Ok(relative_path) = active_ancestor_path.strip_prefix(worktree.root_name()) {
 9633                target_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9634            }
 9635        }
 9636
 9637        let leaf_entry_id =
 9638            leaf_entry_id.unwrap_or_else(|| panic!("no entry for leaf path {leaf_path:?}"));
 9639        let target_entry_id = target_entry_id
 9640            .unwrap_or_else(|| panic!("no entry for active path {active_ancestor_path:?}"));
 9641        let folded_ancestors = panel
 9642            .state
 9643            .ancestors
 9644            .get_mut(&leaf_entry_id)
 9645            .unwrap_or_else(|| panic!("leaf path {leaf_path:?} should be folded"));
 9646        let ancestor_ids = folded_ancestors.ancestors.clone();
 9647
 9648        let mut depth_for_target = None;
 9649        for depth in 0..ancestor_ids.len() {
 9650            let resolved_entry_id = if depth == 0 {
 9651                leaf_entry_id
 9652            } else {
 9653                ancestor_ids.get(depth).copied().unwrap_or(leaf_entry_id)
 9654            };
 9655            if resolved_entry_id == target_entry_id {
 9656                depth_for_target = Some(depth);
 9657                break;
 9658            }
 9659        }
 9660
 9661        folded_ancestors.current_ancestor_depth = depth_for_target.unwrap_or_else(|| {
 9662            panic!(
 9663                "active path {active_ancestor_path:?} is not part of folded ancestors {ancestor_ids:?}"
 9664            )
 9665        });
 9666    });
 9667}
 9668
 9669fn drag_selection_to(
 9670    panel: &Entity<ProjectPanel>,
 9671    target_path: &str,
 9672    is_file: bool,
 9673    cx: &mut VisualTestContext,
 9674) {
 9675    let target_entry = find_project_entry(panel, target_path, cx)
 9676        .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
 9677
 9678    panel.update_in(cx, |panel, window, cx| {
 9679        let selection = panel
 9680            .selection
 9681            .expect("a selection is required before dragging");
 9682        let drag = DraggedSelection {
 9683            active_selection: SelectedEntry {
 9684                worktree_id: selection.worktree_id,
 9685                entry_id: panel.resolve_entry(selection.entry_id),
 9686            },
 9687            marked_selections: Arc::from(panel.marked_entries.clone()),
 9688        };
 9689        panel.drag_onto(&drag, target_entry, is_file, window, cx);
 9690    });
 9691    cx.executor().run_until_parked();
 9692}
 9693
 9694fn find_project_entry(
 9695    panel: &Entity<ProjectPanel>,
 9696    path: &str,
 9697    cx: &mut VisualTestContext,
 9698) -> Option<ProjectEntryId> {
 9699    let path = rel_path(path);
 9700    panel.update(cx, |panel, cx| {
 9701        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9702            let worktree = worktree.read(cx);
 9703            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9704                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9705            }
 9706        }
 9707        panic!("no worktree for path {path:?}");
 9708    })
 9709}
 9710
 9711fn visible_entries_as_strings(
 9712    panel: &Entity<ProjectPanel>,
 9713    range: Range<usize>,
 9714    cx: &mut VisualTestContext,
 9715) -> Vec<String> {
 9716    let mut result = Vec::new();
 9717    let mut project_entries = HashSet::default();
 9718    let mut has_editor = false;
 9719
 9720    panel.update_in(cx, |panel, window, cx| {
 9721        panel.for_each_visible_entry(range, window, cx, &mut |project_entry, details, _, _| {
 9722            if details.is_editing {
 9723                assert!(!has_editor, "duplicate editor entry");
 9724                has_editor = true;
 9725            } else {
 9726                assert!(
 9727                    project_entries.insert(project_entry),
 9728                    "duplicate project entry {:?} {:?}",
 9729                    project_entry,
 9730                    details
 9731                );
 9732            }
 9733
 9734            let indent = "    ".repeat(details.depth);
 9735            let icon = if details.kind.is_dir() {
 9736                if details.is_expanded { "v " } else { "> " }
 9737            } else {
 9738                "  "
 9739            };
 9740            #[cfg(windows)]
 9741            let filename = details.filename.replace("\\", "/");
 9742            #[cfg(not(windows))]
 9743            let filename = details.filename;
 9744            let name = if details.is_editing {
 9745                format!("[EDITOR: '{}']", filename)
 9746            } else if details.is_processing {
 9747                format!("[PROCESSING: '{}']", filename)
 9748            } else {
 9749                filename
 9750            };
 9751            let selected = if details.is_selected {
 9752                "  <== selected"
 9753            } else {
 9754                ""
 9755            };
 9756            let marked = if details.is_marked {
 9757                "  <== marked"
 9758            } else {
 9759                ""
 9760            };
 9761
 9762            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
 9763        });
 9764    });
 9765
 9766    result
 9767}
 9768
 9769/// Test that missing sort_mode field defaults to DirectoriesFirst
 9770#[gpui::test]
 9771async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
 9772    init_test(cx);
 9773
 9774    // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
 9775    let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
 9776    assert_eq!(
 9777        default_settings.sort_mode,
 9778        settings::ProjectPanelSortMode::DirectoriesFirst,
 9779        "sort_mode should default to DirectoriesFirst"
 9780    );
 9781}
 9782
 9783/// Test sort modes: DirectoriesFirst (default) vs Mixed
 9784#[gpui::test]
 9785async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
 9786    init_test(cx);
 9787
 9788    let fs = FakeFs::new(cx.executor());
 9789    fs.insert_tree(
 9790        "/root",
 9791        json!({
 9792            "zebra.txt": "",
 9793            "Apple": {},
 9794            "banana.rs": "",
 9795            "Carrot": {},
 9796            "aardvark.txt": "",
 9797        }),
 9798    )
 9799    .await;
 9800
 9801    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9802    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9803    let workspace = window
 9804        .read_with(cx, |mw, _| mw.workspace().clone())
 9805        .unwrap();
 9806    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9807    let panel = workspace.update_in(cx, ProjectPanel::new);
 9808    cx.run_until_parked();
 9809
 9810    // Default sort mode should be DirectoriesFirst
 9811    assert_eq!(
 9812        visible_entries_as_strings(&panel, 0..50, cx),
 9813        &[
 9814            "v root",
 9815            "    > Apple",
 9816            "    > Carrot",
 9817            "      aardvark.txt",
 9818            "      banana.rs",
 9819            "      zebra.txt",
 9820        ]
 9821    );
 9822}
 9823
 9824#[gpui::test]
 9825async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
 9826    init_test(cx);
 9827
 9828    let fs = FakeFs::new(cx.executor());
 9829    fs.insert_tree(
 9830        "/root",
 9831        json!({
 9832            "Zebra.txt": "",
 9833            "apple": {},
 9834            "Banana.rs": "",
 9835            "carrot": {},
 9836            "Aardvark.txt": "",
 9837        }),
 9838    )
 9839    .await;
 9840
 9841    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9842    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9843    let workspace = window
 9844        .read_with(cx, |mw, _| mw.workspace().clone())
 9845        .unwrap();
 9846    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9847
 9848    // Switch to Mixed mode
 9849    cx.update(|_, cx| {
 9850        cx.update_global::<SettingsStore, _>(|store, cx| {
 9851            store.update_user_settings(cx, |settings| {
 9852                settings.project_panel.get_or_insert_default().sort_mode =
 9853                    Some(settings::ProjectPanelSortMode::Mixed);
 9854            });
 9855        });
 9856    });
 9857
 9858    let panel = workspace.update_in(cx, ProjectPanel::new);
 9859    cx.run_until_parked();
 9860
 9861    // Mixed mode: case-insensitive sorting
 9862    // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
 9863    assert_eq!(
 9864        visible_entries_as_strings(&panel, 0..50, cx),
 9865        &[
 9866            "v root",
 9867            "      Aardvark.txt",
 9868            "    > apple",
 9869            "      Banana.rs",
 9870            "    > carrot",
 9871            "      Zebra.txt",
 9872        ]
 9873    );
 9874}
 9875
 9876#[gpui::test]
 9877async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
 9878    init_test(cx);
 9879
 9880    let fs = FakeFs::new(cx.executor());
 9881    fs.insert_tree(
 9882        "/root",
 9883        json!({
 9884            "Zebra.txt": "",
 9885            "apple": {},
 9886            "Banana.rs": "",
 9887            "carrot": {},
 9888            "Aardvark.txt": "",
 9889        }),
 9890    )
 9891    .await;
 9892
 9893    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9894    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9895    let workspace = window
 9896        .read_with(cx, |mw, _| mw.workspace().clone())
 9897        .unwrap();
 9898    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9899
 9900    // Switch to FilesFirst mode
 9901    cx.update(|_, cx| {
 9902        cx.update_global::<SettingsStore, _>(|store, cx| {
 9903            store.update_user_settings(cx, |settings| {
 9904                settings.project_panel.get_or_insert_default().sort_mode =
 9905                    Some(settings::ProjectPanelSortMode::FilesFirst);
 9906            });
 9907        });
 9908    });
 9909
 9910    let panel = workspace.update_in(cx, ProjectPanel::new);
 9911    cx.run_until_parked();
 9912
 9913    // FilesFirst mode: files first, then directories (both case-insensitive)
 9914    assert_eq!(
 9915        visible_entries_as_strings(&panel, 0..50, cx),
 9916        &[
 9917            "v root",
 9918            "      Aardvark.txt",
 9919            "      Banana.rs",
 9920            "      Zebra.txt",
 9921            "    > apple",
 9922            "    > carrot",
 9923        ]
 9924    );
 9925}
 9926
 9927#[gpui::test]
 9928async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
 9929    init_test(cx);
 9930
 9931    let fs = FakeFs::new(cx.executor());
 9932    fs.insert_tree(
 9933        "/root",
 9934        json!({
 9935            "file2.txt": "",
 9936            "dir1": {},
 9937            "file1.txt": "",
 9938        }),
 9939    )
 9940    .await;
 9941
 9942    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9943    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9944    let workspace = window
 9945        .read_with(cx, |mw, _| mw.workspace().clone())
 9946        .unwrap();
 9947    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9948    let panel = workspace.update_in(cx, ProjectPanel::new);
 9949    cx.run_until_parked();
 9950
 9951    // Initially DirectoriesFirst
 9952    assert_eq!(
 9953        visible_entries_as_strings(&panel, 0..50, cx),
 9954        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
 9955    );
 9956
 9957    // Toggle to Mixed
 9958    cx.update(|_, cx| {
 9959        cx.update_global::<SettingsStore, _>(|store, cx| {
 9960            store.update_user_settings(cx, |settings| {
 9961                settings.project_panel.get_or_insert_default().sort_mode =
 9962                    Some(settings::ProjectPanelSortMode::Mixed);
 9963            });
 9964        });
 9965    });
 9966    cx.run_until_parked();
 9967
 9968    assert_eq!(
 9969        visible_entries_as_strings(&panel, 0..50, cx),
 9970        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
 9971    );
 9972
 9973    // Toggle back to DirectoriesFirst
 9974    cx.update(|_, cx| {
 9975        cx.update_global::<SettingsStore, _>(|store, cx| {
 9976            store.update_user_settings(cx, |settings| {
 9977                settings.project_panel.get_or_insert_default().sort_mode =
 9978                    Some(settings::ProjectPanelSortMode::DirectoriesFirst);
 9979            });
 9980        });
 9981    });
 9982    cx.run_until_parked();
 9983
 9984    assert_eq!(
 9985        visible_entries_as_strings(&panel, 0..50, cx),
 9986        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
 9987    );
 9988}
 9989
 9990#[gpui::test]
 9991async fn test_ensure_temporary_folding_when_creating_in_different_nested_dirs(
 9992    cx: &mut gpui::TestAppContext,
 9993) {
 9994    init_test(cx);
 9995
 9996    // parent: accept
 9997    run_create_file_in_folded_path_case(
 9998        "parent",
 9999        "root1/parent",
10000        "file_in_parent.txt",
10001        &[
10002            "v root1",
10003            "    v parent",
10004            "        > subdir/child",
10005            "          [EDITOR: '']  <== selected",
10006        ],
10007        &[
10008            "v root1",
10009            "    v parent",
10010            "        > subdir/child",
10011            "          file_in_parent.txt  <== selected  <== marked",
10012        ],
10013        true,
10014        cx,
10015    )
10016    .await;
10017
10018    // parent: cancel
10019    run_create_file_in_folded_path_case(
10020        "parent",
10021        "root1/parent",
10022        "file_in_parent.txt",
10023        &[
10024            "v root1",
10025            "    v parent",
10026            "        > subdir/child",
10027            "          [EDITOR: '']  <== selected",
10028        ],
10029        &["v root1", "    > parent/subdir/child  <== selected"],
10030        false,
10031        cx,
10032    )
10033    .await;
10034
10035    // subdir: accept
10036    run_create_file_in_folded_path_case(
10037        "subdir",
10038        "root1/parent/subdir",
10039        "file_in_subdir.txt",
10040        &[
10041            "v root1",
10042            "    v parent/subdir",
10043            "        > child",
10044            "          [EDITOR: '']  <== selected",
10045        ],
10046        &[
10047            "v root1",
10048            "    v parent/subdir",
10049            "        > child",
10050            "          file_in_subdir.txt  <== selected  <== marked",
10051        ],
10052        true,
10053        cx,
10054    )
10055    .await;
10056
10057    // subdir: cancel
10058    run_create_file_in_folded_path_case(
10059        "subdir",
10060        "root1/parent/subdir",
10061        "file_in_subdir.txt",
10062        &[
10063            "v root1",
10064            "    v parent/subdir",
10065            "        > child",
10066            "          [EDITOR: '']  <== selected",
10067        ],
10068        &["v root1", "    > parent/subdir/child  <== selected"],
10069        false,
10070        cx,
10071    )
10072    .await;
10073
10074    // child: accept
10075    run_create_file_in_folded_path_case(
10076        "child",
10077        "root1/parent/subdir/child",
10078        "file_in_child.txt",
10079        &[
10080            "v root1",
10081            "    v parent/subdir/child",
10082            "          [EDITOR: '']  <== selected",
10083        ],
10084        &[
10085            "v root1",
10086            "    v parent/subdir/child",
10087            "          file_in_child.txt  <== selected  <== marked",
10088        ],
10089        true,
10090        cx,
10091    )
10092    .await;
10093
10094    // child: cancel
10095    run_create_file_in_folded_path_case(
10096        "child",
10097        "root1/parent/subdir/child",
10098        "file_in_child.txt",
10099        &[
10100            "v root1",
10101            "    v parent/subdir/child",
10102            "          [EDITOR: '']  <== selected",
10103        ],
10104        &["v root1", "    v parent/subdir/child  <== selected"],
10105        false,
10106        cx,
10107    )
10108    .await;
10109}
10110
10111#[gpui::test]
10112async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu(
10113    cx: &mut gpui::TestAppContext,
10114) {
10115    init_test(cx);
10116
10117    let fs = FakeFs::new(cx.executor());
10118    fs.insert_tree(
10119        "/root1",
10120        json!({
10121            "parent": {
10122                "subdir": {
10123                    "child": {},
10124                }
10125            }
10126        }),
10127    )
10128    .await;
10129
10130    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
10131    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10132    let workspace = window
10133        .read_with(cx, |mw, _| mw.workspace().clone())
10134        .unwrap();
10135    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10136
10137    let panel = workspace.update_in(cx, |workspace, window, cx| {
10138        let panel = ProjectPanel::new(workspace, window, cx);
10139        workspace.add_panel(panel.clone(), window, cx);
10140        panel
10141    });
10142
10143    cx.update(|_, cx| {
10144        let settings = *ProjectPanelSettings::get_global(cx);
10145        ProjectPanelSettings::override_global(
10146            ProjectPanelSettings {
10147                auto_fold_dirs: true,
10148                ..settings
10149            },
10150            cx,
10151        );
10152    });
10153
10154    panel.update_in(cx, |panel, window, cx| {
10155        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
10156    });
10157    cx.run_until_parked();
10158
10159    select_folded_path_with_mark(
10160        &panel,
10161        "root1/parent/subdir/child",
10162        "root1/parent/subdir",
10163        cx,
10164    );
10165    panel.update(cx, |panel, _| {
10166        panel.marked_entries.clear();
10167    });
10168
10169    let parent_entry_id = find_project_entry(&panel, "root1/parent", cx)
10170        .expect("parent directory should exist for this test");
10171    let subdir_entry_id = find_project_entry(&panel, "root1/parent/subdir", cx)
10172        .expect("subdir directory should exist for this test");
10173    let child_entry_id = find_project_entry(&panel, "root1/parent/subdir/child", cx)
10174        .expect("child directory should exist for this test");
10175
10176    panel.update(cx, |panel, _| {
10177        let selection = panel
10178            .selection
10179            .expect("leaf directory should be selected before creating a new entry");
10180        assert_eq!(
10181            selection.entry_id, child_entry_id,
10182            "initial selection should be the folded leaf entry"
10183        );
10184        assert_eq!(
10185            panel.resolve_entry(selection.entry_id),
10186            subdir_entry_id,
10187            "active folded component should start at subdir"
10188        );
10189    });
10190
10191    panel.update_in(cx, |panel, window, cx| {
10192        panel.deploy_context_menu(
10193            gpui::point(gpui::px(1.), gpui::px(1.)),
10194            child_entry_id,
10195            window,
10196            cx,
10197        );
10198        panel.new_file(&NewFile, window, cx);
10199    });
10200    cx.run_until_parked();
10201    panel.update_in(cx, |panel, window, cx| {
10202        assert!(panel.filename_editor.read(cx).is_focused(window));
10203    });
10204    cx.run_until_parked();
10205
10206    set_folded_active_ancestor(&panel, "root1/parent/subdir", "root1/parent", cx);
10207
10208    panel.update_in(cx, |panel, window, cx| {
10209        panel.deploy_context_menu(
10210            gpui::point(gpui::px(2.), gpui::px(2.)),
10211            subdir_entry_id,
10212            window,
10213            cx,
10214        );
10215    });
10216    cx.run_until_parked();
10217
10218    panel.update(cx, |panel, _| {
10219        assert!(
10220            panel.state.edit_state.is_none(),
10221            "opening another context menu should blur the filename editor and discard edit state"
10222        );
10223        let selection = panel
10224            .selection
10225            .expect("selection should restore to the previously focused leaf entry");
10226        assert_eq!(
10227            selection.entry_id, child_entry_id,
10228            "blur-driven cancellation should restore the previous leaf selection"
10229        );
10230        assert_eq!(
10231            panel.resolve_entry(selection.entry_id),
10232            parent_entry_id,
10233            "temporary unfolded pending state should preserve the active ancestor chosen before blur"
10234        );
10235    });
10236
10237    panel.update_in(cx, |panel, window, cx| {
10238        panel.new_file(&NewFile, window, cx);
10239    });
10240    cx.run_until_parked();
10241    assert_eq!(
10242        visible_entries_as_strings(&panel, 0..10, cx),
10243        &[
10244            "v root1",
10245            "    v parent",
10246            "        > subdir/child",
10247            "          [EDITOR: '']  <== selected",
10248        ],
10249        "new file after blur should use the preserved active ancestor"
10250    );
10251    panel.update(cx, |panel, _| {
10252        let edit_state = panel
10253            .state
10254            .edit_state
10255            .as_ref()
10256            .expect("new file should enter edit state");
10257        assert_eq!(
10258            edit_state.temporarily_unfolded,
10259            Some(parent_entry_id),
10260            "temporary unfolding should now target parent after restoring the active ancestor"
10261        );
10262    });
10263
10264    let file_name = "created_after_blur.txt";
10265    panel
10266        .update_in(cx, |panel, window, cx| {
10267            panel.filename_editor.update(cx, |editor, cx| {
10268                editor.set_text(file_name, window, cx);
10269            });
10270            panel.confirm_edit(true, window, cx).expect(
10271                "confirm_edit should start creation for the file created after blur transition",
10272            )
10273        })
10274        .await
10275        .expect("creating file after blur transition should succeed");
10276    cx.run_until_parked();
10277
10278    assert!(
10279        fs.is_file(Path::new("/root1/parent/created_after_blur.txt"))
10280            .await,
10281        "file should be created under parent after active ancestor is restored to parent"
10282    );
10283    assert!(
10284        !fs.is_file(Path::new("/root1/parent/subdir/created_after_blur.txt"))
10285            .await,
10286        "file should not be created under subdir when parent is the active ancestor"
10287    );
10288}
10289
10290async fn run_create_file_in_folded_path_case(
10291    case_name: &str,
10292    active_ancestor_path: &str,
10293    created_file_name: &str,
10294    expected_temporary_state: &[&str],
10295    expected_final_state: &[&str],
10296    accept_creation: bool,
10297    cx: &mut gpui::TestAppContext,
10298) {
10299    let expected_collapsed_state = &["v root1", "    > parent/subdir/child  <== selected"];
10300
10301    let fs = FakeFs::new(cx.executor());
10302    fs.insert_tree(
10303        "/root1",
10304        json!({
10305            "parent": {
10306                "subdir": {
10307                    "child": {},
10308                }
10309            }
10310        }),
10311    )
10312    .await;
10313
10314    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
10315    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10316    let workspace = window
10317        .read_with(cx, |mw, _| mw.workspace().clone())
10318        .unwrap();
10319    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10320
10321    let panel = workspace.update_in(cx, |workspace, window, cx| {
10322        let panel = ProjectPanel::new(workspace, window, cx);
10323        workspace.add_panel(panel.clone(), window, cx);
10324        panel
10325    });
10326
10327    cx.update(|_, cx| {
10328        let settings = *ProjectPanelSettings::get_global(cx);
10329        ProjectPanelSettings::override_global(
10330            ProjectPanelSettings {
10331                auto_fold_dirs: true,
10332                ..settings
10333            },
10334            cx,
10335        );
10336    });
10337
10338    panel.update_in(cx, |panel, window, cx| {
10339        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
10340    });
10341    cx.run_until_parked();
10342
10343    select_folded_path_with_mark(
10344        &panel,
10345        "root1/parent/subdir/child",
10346        active_ancestor_path,
10347        cx,
10348    );
10349    panel.update(cx, |panel, _| {
10350        panel.marked_entries.clear();
10351    });
10352
10353    assert_eq!(
10354        visible_entries_as_strings(&panel, 0..10, cx),
10355        expected_collapsed_state,
10356        "case '{}' should start from a folded state",
10357        case_name
10358    );
10359
10360    panel.update_in(cx, |panel, window, cx| {
10361        panel.new_file(&NewFile, window, cx);
10362    });
10363    cx.run_until_parked();
10364    panel.update_in(cx, |panel, window, cx| {
10365        assert!(panel.filename_editor.read(cx).is_focused(window));
10366    });
10367    cx.run_until_parked();
10368    assert_eq!(
10369        visible_entries_as_strings(&panel, 0..10, cx),
10370        expected_temporary_state,
10371        "case '{}' ({}) should temporarily unfold the active ancestor while editing",
10372        case_name,
10373        if accept_creation { "accept" } else { "cancel" }
10374    );
10375
10376    let relative_directory = active_ancestor_path
10377        .strip_prefix("root1/")
10378        .expect("active_ancestor_path should start with root1/");
10379    let created_file_path = PathBuf::from("/root1")
10380        .join(relative_directory)
10381        .join(created_file_name);
10382
10383    if accept_creation {
10384        panel
10385            .update_in(cx, |panel, window, cx| {
10386                panel.filename_editor.update(cx, |editor, cx| {
10387                    editor.set_text(created_file_name, window, cx);
10388                });
10389                panel.confirm_edit(true, window, cx).unwrap()
10390            })
10391            .await
10392            .unwrap();
10393        cx.run_until_parked();
10394
10395        assert_eq!(
10396            visible_entries_as_strings(&panel, 0..10, cx),
10397            expected_final_state,
10398            "case '{}' should keep the newly created file selected and marked after accept",
10399            case_name
10400        );
10401        assert!(
10402            fs.is_file(created_file_path.as_path()).await,
10403            "case '{}' should create file '{}'",
10404            case_name,
10405            created_file_path.display()
10406        );
10407    } else {
10408        panel.update_in(cx, |panel, window, cx| {
10409            panel.cancel(&Cancel, window, cx);
10410        });
10411        cx.run_until_parked();
10412
10413        assert_eq!(
10414            visible_entries_as_strings(&panel, 0..10, cx),
10415            expected_final_state,
10416            "case '{}' should keep the expected panel state after cancel",
10417            case_name
10418        );
10419        assert!(
10420            !fs.is_file(created_file_path.as_path()).await,
10421            "case '{}' should not create a file after cancel",
10422            case_name
10423        );
10424    }
10425}
10426
10427pub(crate) fn init_test(cx: &mut TestAppContext) {
10428    cx.update(|cx| {
10429        let settings_store = SettingsStore::test(cx);
10430        cx.set_global(settings_store);
10431        theme_settings::init(theme::LoadThemes::JustBase, cx);
10432        crate::init(cx);
10433
10434        cx.update_global::<SettingsStore, _>(|store, cx| {
10435            store.update_user_settings(cx, |settings| {
10436                settings
10437                    .project_panel
10438                    .get_or_insert_default()
10439                    .auto_fold_dirs = Some(false);
10440                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
10441            });
10442        });
10443    });
10444}
10445
10446fn init_test_with_editor(cx: &mut TestAppContext) {
10447    cx.update(|cx| {
10448        let app_state = AppState::test(cx);
10449        theme_settings::init(theme::LoadThemes::JustBase, cx);
10450        editor::init(cx);
10451        crate::init(cx);
10452        workspace::init(app_state, cx);
10453
10454        cx.update_global::<SettingsStore, _>(|store, cx| {
10455            store.update_user_settings(cx, |settings| {
10456                settings
10457                    .project_panel
10458                    .get_or_insert_default()
10459                    .auto_fold_dirs = Some(false);
10460                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
10461            });
10462        });
10463    });
10464}
10465
10466fn set_auto_open_settings(
10467    cx: &mut TestAppContext,
10468    auto_open_settings: ProjectPanelAutoOpenSettings,
10469) {
10470    cx.update(|cx| {
10471        cx.update_global::<SettingsStore, _>(|store, cx| {
10472            store.update_user_settings(cx, |settings| {
10473                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
10474            });
10475        })
10476    });
10477}
10478
10479fn ensure_single_file_is_opened(
10480    workspace: &Entity<Workspace>,
10481    expected_path: &str,
10482    cx: &mut VisualTestContext,
10483) {
10484    workspace.update_in(cx, |workspace, _, cx| {
10485        let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
10486        assert_eq!(worktrees.len(), 1);
10487        let worktree_id = worktrees[0].read(cx).id();
10488
10489        let open_project_paths = workspace
10490            .panes()
10491            .iter()
10492            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
10493            .collect::<Vec<_>>();
10494        assert_eq!(
10495            open_project_paths,
10496            vec![ProjectPath {
10497                worktree_id,
10498                path: Arc::from(rel_path(expected_path))
10499            }],
10500            "Should have opened file, selected in project panel"
10501        );
10502    });
10503}
10504
10505fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
10506    assert!(
10507        !cx.has_pending_prompt(),
10508        "Should have no prompts before the deletion"
10509    );
10510    panel.update_in(cx, |panel, window, cx| {
10511        panel.delete(&Delete { skip_prompt: false }, window, cx)
10512    });
10513    assert!(
10514        cx.has_pending_prompt(),
10515        "Should have a prompt after the deletion"
10516    );
10517    cx.simulate_prompt_answer("Delete");
10518    assert!(
10519        !cx.has_pending_prompt(),
10520        "Should have no prompts after prompt was replied to"
10521    );
10522    cx.executor().run_until_parked();
10523}
10524
10525fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
10526    assert!(
10527        !cx.has_pending_prompt(),
10528        "Should have no prompts before the deletion"
10529    );
10530    panel.update_in(cx, |panel, window, cx| {
10531        panel.delete(&Delete { skip_prompt: true }, window, cx)
10532    });
10533    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
10534    cx.executor().run_until_parked();
10535}
10536
10537fn ensure_no_open_items_and_panes(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) {
10538    assert!(
10539        !cx.has_pending_prompt(),
10540        "Should have no prompts after deletion operation closes the file"
10541    );
10542    workspace.update_in(cx, |workspace, _window, cx| {
10543        let open_project_paths = workspace
10544            .panes()
10545            .iter()
10546            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
10547            .collect::<Vec<_>>();
10548        assert!(
10549            open_project_paths.is_empty(),
10550            "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
10551        );
10552    });
10553}
10554
10555struct TestProjectItemView {
10556    focus_handle: FocusHandle,
10557    path: ProjectPath,
10558}
10559
10560struct TestProjectItem {
10561    path: ProjectPath,
10562}
10563
10564impl project::ProjectItem for TestProjectItem {
10565    fn try_open(
10566        _project: &Entity<Project>,
10567        path: &ProjectPath,
10568        cx: &mut App,
10569    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10570        let path = path.clone();
10571        Some(cx.spawn(async move |cx| Ok(cx.new(|_| Self { path }))))
10572    }
10573
10574    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10575        None
10576    }
10577
10578    fn project_path(&self, _: &App) -> Option<ProjectPath> {
10579        Some(self.path.clone())
10580    }
10581
10582    fn is_dirty(&self) -> bool {
10583        false
10584    }
10585}
10586
10587impl ProjectItem for TestProjectItemView {
10588    type Item = TestProjectItem;
10589
10590    fn for_project_item(
10591        _: Entity<Project>,
10592        _: Option<&Pane>,
10593        project_item: Entity<Self::Item>,
10594        _: &mut Window,
10595        cx: &mut Context<Self>,
10596    ) -> Self
10597    where
10598        Self: Sized,
10599    {
10600        Self {
10601            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
10602            focus_handle: cx.focus_handle(),
10603        }
10604    }
10605}
10606
10607impl Item for TestProjectItemView {
10608    type Event = ();
10609
10610    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10611        "Test".into()
10612    }
10613}
10614
10615impl EventEmitter<()> for TestProjectItemView {}
10616
10617impl Focusable for TestProjectItemView {
10618    fn focus_handle(&self, _: &App) -> FocusHandle {
10619        self.focus_handle.clone()
10620    }
10621}
10622
10623impl Render for TestProjectItemView {
10624    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
10625        Empty
10626    }
10627}