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, test::TestItem},
   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(0),
 1258                "Should select from the beginning of the filename"
 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_reveal_in_project_panel_fallback(cx: &mut gpui::TestAppContext) {
 6020    init_test_with_editor(cx);
 6021    let fs = FakeFs::new(cx.background_executor.clone());
 6022    fs.insert_tree(
 6023        "/workspace",
 6024        json!({
 6025            "README.md": ""
 6026        }),
 6027    )
 6028    .await;
 6029
 6030    let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await;
 6031    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6032    let workspace = window
 6033        .read_with(cx, |mw, _| mw.workspace().clone())
 6034        .unwrap();
 6035    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6036    let panel = workspace.update_in(cx, |workspace, window, cx| {
 6037        let panel = ProjectPanel::new(workspace, window, cx);
 6038        workspace.add_panel(panel.clone(), window, cx);
 6039        panel
 6040    });
 6041    cx.run_until_parked();
 6042
 6043    // Project panel should still be activated and focused, when using `pane:
 6044    // reveal in project panel` without an active item.
 6045    cx.dispatch_action(workspace::RevealInProjectPanel::default());
 6046    cx.run_until_parked();
 6047
 6048    panel.update_in(cx, |panel, window, cx| {
 6049        panel
 6050            .workspace
 6051            .update(cx, |workspace, cx| {
 6052                assert!(
 6053                    workspace.active_item(cx).is_none(),
 6054                    "Workspace should not have an active item."
 6055                );
 6056            })
 6057            .unwrap();
 6058
 6059        assert!(
 6060            panel.focus_handle(cx).is_focused(window),
 6061            "Project panel should be focused, even when there's no active item."
 6062        );
 6063    });
 6064
 6065    // When working with a file that doesn't belong to an open project, we
 6066    // should still activate the project panel on `pane: reveal in project
 6067    // panel`.
 6068    fs.insert_tree(
 6069        "/external",
 6070        json!({
 6071            "file.txt": "External File",
 6072        }),
 6073    )
 6074    .await;
 6075
 6076    let (worktree, _) = project
 6077        .update(cx, |project, cx| {
 6078            project.find_or_create_worktree("/external/file.txt", false, cx)
 6079        })
 6080        .await
 6081        .unwrap();
 6082
 6083    workspace
 6084        .update_in(cx, |workspace, window, cx| {
 6085            let worktree_id = worktree.read(cx).id();
 6086            let path = rel_path("").into();
 6087            let project_path = ProjectPath { worktree_id, path };
 6088
 6089            workspace.open_path(project_path, None, true, window, cx)
 6090        })
 6091        .await
 6092        .unwrap();
 6093    cx.run_until_parked();
 6094
 6095    panel.update_in(cx, |panel, window, cx| {
 6096        assert!(
 6097            !panel.focus_handle(cx).is_focused(window),
 6098            "Project panel should not be focused after opening an external file."
 6099        );
 6100    });
 6101
 6102    cx.dispatch_action(workspace::RevealInProjectPanel::default());
 6103    cx.run_until_parked();
 6104
 6105    panel.update_in(cx, |panel, window, cx| {
 6106        panel
 6107            .workspace
 6108            .update(cx, |workspace, cx| {
 6109                assert!(
 6110                    workspace.active_item(cx).is_some(),
 6111                    "Workspace should have an active item."
 6112                );
 6113            })
 6114            .unwrap();
 6115
 6116        assert!(
 6117            panel.focus_handle(cx).is_focused(window),
 6118            "Project panel should be focused even for invisible worktree entry."
 6119        );
 6120    });
 6121
 6122    // Focus again on the center pane so we're sure that the focus doesn't
 6123    // remain on the project panel, otherwise later assertions wouldn't matter.
 6124    panel.update_in(cx, |panel, window, cx| {
 6125        panel
 6126            .workspace
 6127            .update(cx, |workspace, cx| {
 6128                workspace.focus_center_pane(window, cx);
 6129            })
 6130            .log_err();
 6131
 6132        assert!(
 6133            !panel.focus_handle(cx).is_focused(window),
 6134            "Project panel should not be focused after focusing on center pane."
 6135        );
 6136    });
 6137
 6138    panel.update_in(cx, |panel, window, cx| {
 6139        assert!(
 6140            !panel.focus_handle(cx).is_focused(window),
 6141            "Project panel should not be focused after focusing the center pane."
 6142        );
 6143    });
 6144
 6145    // Create an unsaved buffer and verify that pane: reveal in project panel`
 6146    // still activates and focuses the panel.
 6147    let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
 6148    pane.update_in(cx, |pane, window, cx| {
 6149        let item = cx.new(|cx| TestItem::new(cx).with_label("Unsaved buffer"));
 6150        pane.add_item(Box::new(item), false, false, None, window, cx);
 6151    });
 6152
 6153    cx.dispatch_action(workspace::RevealInProjectPanel::default());
 6154    cx.run_until_parked();
 6155
 6156    panel.update_in(cx, |panel, window, cx| {
 6157        panel
 6158            .workspace
 6159            .update(cx, |workspace, cx| {
 6160                assert!(
 6161                    workspace.active_item(cx).is_some(),
 6162                    "Workspace should have an active item."
 6163                );
 6164            })
 6165            .unwrap();
 6166
 6167        assert!(
 6168            panel.focus_handle(cx).is_focused(window),
 6169            "Project panel should be focused even for an unsaved buffer."
 6170        );
 6171    });
 6172}
 6173
 6174#[gpui::test]
 6175async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
 6176    init_test(cx);
 6177    cx.update(|cx| {
 6178        cx.update_global::<SettingsStore, _>(|store, cx| {
 6179            store.update_user_settings(cx, |settings| {
 6180                settings.project.worktree.file_scan_exclusions =
 6181                    Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
 6182            });
 6183        });
 6184    });
 6185
 6186    cx.update(|cx| {
 6187        register_project_item::<TestProjectItemView>(cx);
 6188    });
 6189
 6190    let fs = FakeFs::new(cx.executor());
 6191    fs.insert_tree(
 6192        "/root1",
 6193        json!({
 6194            ".dockerignore": "",
 6195            ".git": {
 6196                "HEAD": "",
 6197            },
 6198        }),
 6199    )
 6200    .await;
 6201
 6202    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 6203    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6204    let workspace = window
 6205        .read_with(cx, |mw, _| mw.workspace().clone())
 6206        .unwrap();
 6207    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6208    let panel = workspace.update_in(cx, |workspace, window, cx| {
 6209        let panel = ProjectPanel::new(workspace, window, cx);
 6210        workspace.add_panel(panel.clone(), window, cx);
 6211        panel
 6212    });
 6213    cx.run_until_parked();
 6214
 6215    select_path(&panel, "root1", cx);
 6216    assert_eq!(
 6217        visible_entries_as_strings(&panel, 0..10, cx),
 6218        &["v root1  <== selected", "      .dockerignore",]
 6219    );
 6220    workspace.update_in(cx, |workspace, _, cx| {
 6221        assert!(
 6222            workspace.active_item(cx).is_none(),
 6223            "Should have no active items in the beginning"
 6224        );
 6225    });
 6226
 6227    let excluded_file_path = ".git/COMMIT_EDITMSG";
 6228    let excluded_dir_path = "excluded_dir";
 6229
 6230    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
 6231    cx.run_until_parked();
 6232    panel.update_in(cx, |panel, window, cx| {
 6233        assert!(panel.filename_editor.read(cx).is_focused(window));
 6234    });
 6235    panel
 6236        .update_in(cx, |panel, window, cx| {
 6237            panel.filename_editor.update(cx, |editor, cx| {
 6238                editor.set_text(excluded_file_path, window, cx)
 6239            });
 6240            panel.confirm_edit(true, window, cx).unwrap()
 6241        })
 6242        .await
 6243        .unwrap();
 6244
 6245    assert_eq!(
 6246        visible_entries_as_strings(&panel, 0..13, cx),
 6247        &["v root1", "      .dockerignore"],
 6248        "Excluded dir should not be shown after opening a file in it"
 6249    );
 6250    panel.update_in(cx, |panel, window, cx| {
 6251        assert!(
 6252            !panel.filename_editor.read(cx).is_focused(window),
 6253            "Should have closed the file name editor"
 6254        );
 6255    });
 6256    workspace.update_in(cx, |workspace, _, cx| {
 6257        let active_entry_path = workspace
 6258            .active_item(cx)
 6259            .expect("should have opened and activated the excluded item")
 6260            .act_as::<TestProjectItemView>(cx)
 6261            .expect("should have opened the corresponding project item for the excluded item")
 6262            .read(cx)
 6263            .path
 6264            .clone();
 6265        assert_eq!(
 6266            active_entry_path.path.as_ref(),
 6267            rel_path(excluded_file_path),
 6268            "Should open the excluded file"
 6269        );
 6270
 6271        assert!(
 6272            workspace.notification_ids().is_empty(),
 6273            "Should have no notifications after opening an excluded file"
 6274        );
 6275    });
 6276    assert!(
 6277        fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
 6278        "Should have created the excluded file"
 6279    );
 6280
 6281    select_path(&panel, "root1", cx);
 6282    panel.update_in(cx, |panel, window, cx| {
 6283        panel.new_directory(&NewDirectory, window, cx)
 6284    });
 6285    cx.run_until_parked();
 6286    panel.update_in(cx, |panel, window, cx| {
 6287        assert!(panel.filename_editor.read(cx).is_focused(window));
 6288    });
 6289    panel
 6290        .update_in(cx, |panel, window, cx| {
 6291            panel.filename_editor.update(cx, |editor, cx| {
 6292                editor.set_text(excluded_file_path, window, cx)
 6293            });
 6294            panel.confirm_edit(true, window, cx).unwrap()
 6295        })
 6296        .await
 6297        .unwrap();
 6298    cx.run_until_parked();
 6299    assert_eq!(
 6300        visible_entries_as_strings(&panel, 0..13, cx),
 6301        &["v root1", "      .dockerignore"],
 6302        "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
 6303    );
 6304    panel.update_in(cx, |panel, window, cx| {
 6305        assert!(
 6306            !panel.filename_editor.read(cx).is_focused(window),
 6307            "Should have closed the file name editor"
 6308        );
 6309    });
 6310    workspace.update_in(cx, |workspace, _, cx| {
 6311        let notifications = workspace.notification_ids();
 6312        assert_eq!(
 6313            notifications.len(),
 6314            1,
 6315            "Should receive one notification with the error message"
 6316        );
 6317        workspace.dismiss_notification(notifications.first().unwrap(), cx);
 6318        assert!(workspace.notification_ids().is_empty());
 6319    });
 6320
 6321    select_path(&panel, "root1", cx);
 6322    panel.update_in(cx, |panel, window, cx| {
 6323        panel.new_directory(&NewDirectory, window, cx)
 6324    });
 6325    cx.run_until_parked();
 6326
 6327    panel.update_in(cx, |panel, window, cx| {
 6328        assert!(panel.filename_editor.read(cx).is_focused(window));
 6329    });
 6330
 6331    panel
 6332        .update_in(cx, |panel, window, cx| {
 6333            panel.filename_editor.update(cx, |editor, cx| {
 6334                editor.set_text(excluded_dir_path, window, cx)
 6335            });
 6336            panel.confirm_edit(true, window, cx).unwrap()
 6337        })
 6338        .await
 6339        .unwrap();
 6340
 6341    cx.run_until_parked();
 6342
 6343    assert_eq!(
 6344        visible_entries_as_strings(&panel, 0..13, cx),
 6345        &["v root1", "      .dockerignore"],
 6346        "Should not change the project panel after trying to create an excluded directory"
 6347    );
 6348    panel.update_in(cx, |panel, window, cx| {
 6349        assert!(
 6350            !panel.filename_editor.read(cx).is_focused(window),
 6351            "Should have closed the file name editor"
 6352        );
 6353    });
 6354    workspace.update_in(cx, |workspace, _, cx| {
 6355        let notifications = workspace.notification_ids();
 6356        assert_eq!(
 6357            notifications.len(),
 6358            1,
 6359            "Should receive one notification explaining that no directory is actually shown"
 6360        );
 6361        workspace.dismiss_notification(notifications.first().unwrap(), cx);
 6362        assert!(workspace.notification_ids().is_empty());
 6363    });
 6364    assert!(
 6365        fs.is_dir(Path::new("/root1/excluded_dir")).await,
 6366        "Should have created the excluded directory"
 6367    );
 6368}
 6369
 6370#[gpui::test]
 6371async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
 6372    init_test_with_editor(cx);
 6373
 6374    let fs = FakeFs::new(cx.executor());
 6375    fs.insert_tree(
 6376        "/src",
 6377        json!({
 6378            "test": {
 6379                "first.rs": "// First Rust file",
 6380                "second.rs": "// Second Rust file",
 6381                "third.rs": "// Third Rust file",
 6382            }
 6383        }),
 6384    )
 6385    .await;
 6386
 6387    let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
 6388    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6389    let workspace = window
 6390        .read_with(cx, |mw, _| mw.workspace().clone())
 6391        .unwrap();
 6392    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6393    let panel = workspace.update_in(cx, |workspace, window, cx| {
 6394        let panel = ProjectPanel::new(workspace, window, cx);
 6395        workspace.add_panel(panel.clone(), window, cx);
 6396        panel
 6397    });
 6398    cx.run_until_parked();
 6399
 6400    select_path(&panel, "src", cx);
 6401    panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
 6402    cx.executor().run_until_parked();
 6403    assert_eq!(
 6404        visible_entries_as_strings(&panel, 0..10, cx),
 6405        &[
 6406            //
 6407            "v src  <== selected",
 6408            "    > test"
 6409        ]
 6410    );
 6411    panel.update_in(cx, |panel, window, cx| {
 6412        panel.new_directory(&NewDirectory, window, cx)
 6413    });
 6414    cx.executor().run_until_parked();
 6415    panel.update_in(cx, |panel, window, cx| {
 6416        assert!(panel.filename_editor.read(cx).is_focused(window));
 6417    });
 6418    assert_eq!(
 6419        visible_entries_as_strings(&panel, 0..10, cx),
 6420        &[
 6421            //
 6422            "v src",
 6423            "    > [EDITOR: '']  <== selected",
 6424            "    > test"
 6425        ]
 6426    );
 6427
 6428    panel.update_in(cx, |panel, window, cx| {
 6429        panel.cancel(&menu::Cancel, window, cx);
 6430    });
 6431    cx.executor().run_until_parked();
 6432    assert_eq!(
 6433        visible_entries_as_strings(&panel, 0..10, cx),
 6434        &[
 6435            //
 6436            "v src  <== selected",
 6437            "    > test"
 6438        ]
 6439    );
 6440
 6441    panel.update_in(cx, |panel, window, cx| {
 6442        panel.new_directory(&NewDirectory, window, cx)
 6443    });
 6444    cx.executor().run_until_parked();
 6445    panel.update_in(cx, |panel, window, cx| {
 6446        assert!(panel.filename_editor.read(cx).is_focused(window));
 6447    });
 6448    assert_eq!(
 6449        visible_entries_as_strings(&panel, 0..10, cx),
 6450        &[
 6451            //
 6452            "v src",
 6453            "    > [EDITOR: '']  <== selected",
 6454            "    > test"
 6455        ]
 6456    );
 6457    workspace.update_in(cx, |_, window, _| window.blur());
 6458    cx.executor().run_until_parked();
 6459    assert_eq!(
 6460        visible_entries_as_strings(&panel, 0..10, cx),
 6461        &[
 6462            //
 6463            "v src  <== selected",
 6464            "    > test"
 6465        ]
 6466    );
 6467}
 6468
 6469#[gpui::test]
 6470async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
 6471    init_test_with_editor(cx);
 6472
 6473    let fs = FakeFs::new(cx.executor());
 6474    fs.insert_tree(
 6475        "/root",
 6476        json!({
 6477            "dir1": {
 6478                "subdir1": {},
 6479                "file1.txt": "",
 6480                "file2.txt": "",
 6481            },
 6482            "dir2": {
 6483                "subdir2": {},
 6484                "file3.txt": "",
 6485                "file4.txt": "",
 6486            },
 6487            "file5.txt": "",
 6488            "file6.txt": "",
 6489        }),
 6490    )
 6491    .await;
 6492
 6493    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6494    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6495    let workspace = window
 6496        .read_with(cx, |mw, _| mw.workspace().clone())
 6497        .unwrap();
 6498    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6499    let panel = workspace.update_in(cx, ProjectPanel::new);
 6500    cx.run_until_parked();
 6501
 6502    toggle_expand_dir(&panel, "root/dir1", cx);
 6503    toggle_expand_dir(&panel, "root/dir2", cx);
 6504
 6505    // Test Case 1: Delete middle file in directory
 6506    select_path(&panel, "root/dir1/file1.txt", cx);
 6507    assert_eq!(
 6508        visible_entries_as_strings(&panel, 0..15, cx),
 6509        &[
 6510            "v root",
 6511            "    v dir1",
 6512            "        > subdir1",
 6513            "          file1.txt  <== selected",
 6514            "          file2.txt",
 6515            "    v dir2",
 6516            "        > subdir2",
 6517            "          file3.txt",
 6518            "          file4.txt",
 6519            "      file5.txt",
 6520            "      file6.txt",
 6521        ],
 6522        "Initial state before deleting middle file"
 6523    );
 6524
 6525    submit_deletion(&panel, cx);
 6526    assert_eq!(
 6527        visible_entries_as_strings(&panel, 0..15, cx),
 6528        &[
 6529            "v root",
 6530            "    v dir1",
 6531            "        > subdir1",
 6532            "          file2.txt  <== selected",
 6533            "    v dir2",
 6534            "        > subdir2",
 6535            "          file3.txt",
 6536            "          file4.txt",
 6537            "      file5.txt",
 6538            "      file6.txt",
 6539        ],
 6540        "Should select next file after deleting middle file"
 6541    );
 6542
 6543    // Test Case 2: Delete last file in directory
 6544    submit_deletion(&panel, cx);
 6545    assert_eq!(
 6546        visible_entries_as_strings(&panel, 0..15, cx),
 6547        &[
 6548            "v root",
 6549            "    v dir1",
 6550            "        > subdir1  <== selected",
 6551            "    v dir2",
 6552            "        > subdir2",
 6553            "          file3.txt",
 6554            "          file4.txt",
 6555            "      file5.txt",
 6556            "      file6.txt",
 6557        ],
 6558        "Should select next directory when last file is deleted"
 6559    );
 6560
 6561    // Test Case 3: Delete root level file
 6562    select_path(&panel, "root/file6.txt", cx);
 6563    assert_eq!(
 6564        visible_entries_as_strings(&panel, 0..15, cx),
 6565        &[
 6566            "v root",
 6567            "    v dir1",
 6568            "        > subdir1",
 6569            "    v dir2",
 6570            "        > subdir2",
 6571            "          file3.txt",
 6572            "          file4.txt",
 6573            "      file5.txt",
 6574            "      file6.txt  <== selected",
 6575        ],
 6576        "Initial state before deleting root level file"
 6577    );
 6578
 6579    submit_deletion(&panel, cx);
 6580    assert_eq!(
 6581        visible_entries_as_strings(&panel, 0..15, cx),
 6582        &[
 6583            "v root",
 6584            "    v dir1",
 6585            "        > subdir1",
 6586            "    v dir2",
 6587            "        > subdir2",
 6588            "          file3.txt",
 6589            "          file4.txt",
 6590            "      file5.txt  <== selected",
 6591        ],
 6592        "Should select prev entry at root level"
 6593    );
 6594}
 6595
 6596#[gpui::test]
 6597async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
 6598    init_test_with_editor(cx);
 6599
 6600    let fs = FakeFs::new(cx.executor());
 6601    fs.insert_tree(
 6602        path!("/root"),
 6603        json!({
 6604            "aa": "// Testing 1",
 6605            "bb": "// Testing 2",
 6606            "cc": "// Testing 3",
 6607            "dd": "// Testing 4",
 6608            "ee": "// Testing 5",
 6609            "ff": "// Testing 6",
 6610            "gg": "// Testing 7",
 6611            "hh": "// Testing 8",
 6612            "ii": "// Testing 8",
 6613            ".gitignore": "bb\ndd\nee\nff\nii\n'",
 6614        }),
 6615    )
 6616    .await;
 6617
 6618    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 6619    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6620    let workspace = window
 6621        .read_with(cx, |mw, _| mw.workspace().clone())
 6622        .unwrap();
 6623    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6624
 6625    // Test 1: Auto selection with one gitignored file next to the deleted file
 6626    cx.update(|_, cx| {
 6627        let settings = *ProjectPanelSettings::get_global(cx);
 6628        ProjectPanelSettings::override_global(
 6629            ProjectPanelSettings {
 6630                hide_gitignore: true,
 6631                ..settings
 6632            },
 6633            cx,
 6634        );
 6635    });
 6636
 6637    let panel = workspace.update_in(cx, ProjectPanel::new);
 6638    cx.run_until_parked();
 6639
 6640    select_path(&panel, "root/aa", cx);
 6641    assert_eq!(
 6642        visible_entries_as_strings(&panel, 0..10, cx),
 6643        &[
 6644            "v root",
 6645            "      .gitignore",
 6646            "      aa  <== selected",
 6647            "      cc",
 6648            "      gg",
 6649            "      hh"
 6650        ],
 6651        "Initial state should hide files on .gitignore"
 6652    );
 6653
 6654    submit_deletion(&panel, cx);
 6655
 6656    assert_eq!(
 6657        visible_entries_as_strings(&panel, 0..10, cx),
 6658        &[
 6659            "v root",
 6660            "      .gitignore",
 6661            "      cc  <== selected",
 6662            "      gg",
 6663            "      hh"
 6664        ],
 6665        "Should select next entry not on .gitignore"
 6666    );
 6667
 6668    // Test 2: Auto selection with many gitignored files next to the deleted file
 6669    submit_deletion(&panel, cx);
 6670    assert_eq!(
 6671        visible_entries_as_strings(&panel, 0..10, cx),
 6672        &[
 6673            "v root",
 6674            "      .gitignore",
 6675            "      gg  <== selected",
 6676            "      hh"
 6677        ],
 6678        "Should select next entry not on .gitignore"
 6679    );
 6680
 6681    // Test 3: Auto selection of entry before deleted file
 6682    select_path(&panel, "root/hh", cx);
 6683    assert_eq!(
 6684        visible_entries_as_strings(&panel, 0..10, cx),
 6685        &[
 6686            "v root",
 6687            "      .gitignore",
 6688            "      gg",
 6689            "      hh  <== selected"
 6690        ],
 6691        "Should select next entry not on .gitignore"
 6692    );
 6693    submit_deletion(&panel, cx);
 6694    assert_eq!(
 6695        visible_entries_as_strings(&panel, 0..10, cx),
 6696        &["v root", "      .gitignore", "      gg  <== selected"],
 6697        "Should select next entry not on .gitignore"
 6698    );
 6699}
 6700
 6701#[gpui::test]
 6702async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
 6703    init_test_with_editor(cx);
 6704
 6705    let fs = FakeFs::new(cx.executor());
 6706    fs.insert_tree(
 6707        path!("/root"),
 6708        json!({
 6709            "dir1": {
 6710                "file1": "// Testing",
 6711                "file2": "// Testing",
 6712                "file3": "// Testing"
 6713            },
 6714            "aa": "// Testing",
 6715            ".gitignore": "file1\nfile3\n",
 6716        }),
 6717    )
 6718    .await;
 6719
 6720    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 6721    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6722    let workspace = window
 6723        .read_with(cx, |mw, _| mw.workspace().clone())
 6724        .unwrap();
 6725    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6726
 6727    cx.update(|_, cx| {
 6728        let settings = *ProjectPanelSettings::get_global(cx);
 6729        ProjectPanelSettings::override_global(
 6730            ProjectPanelSettings {
 6731                hide_gitignore: true,
 6732                ..settings
 6733            },
 6734            cx,
 6735        );
 6736    });
 6737
 6738    let panel = workspace.update_in(cx, ProjectPanel::new);
 6739    cx.run_until_parked();
 6740
 6741    // Test 1: Visible items should exclude files on gitignore
 6742    toggle_expand_dir(&panel, "root/dir1", cx);
 6743    select_path(&panel, "root/dir1/file2", cx);
 6744    assert_eq!(
 6745        visible_entries_as_strings(&panel, 0..10, cx),
 6746        &[
 6747            "v root",
 6748            "    v dir1",
 6749            "          file2  <== selected",
 6750            "      .gitignore",
 6751            "      aa"
 6752        ],
 6753        "Initial state should hide files on .gitignore"
 6754    );
 6755    submit_deletion(&panel, cx);
 6756
 6757    // Test 2: Auto selection should go to the parent
 6758    assert_eq!(
 6759        visible_entries_as_strings(&panel, 0..10, cx),
 6760        &[
 6761            "v root",
 6762            "    v dir1  <== selected",
 6763            "      .gitignore",
 6764            "      aa"
 6765        ],
 6766        "Initial state should hide files on .gitignore"
 6767    );
 6768}
 6769
 6770#[gpui::test]
 6771async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
 6772    init_test_with_editor(cx);
 6773
 6774    let fs = FakeFs::new(cx.executor());
 6775    fs.insert_tree(
 6776        "/root",
 6777        json!({
 6778            "dir1": {
 6779                "subdir1": {
 6780                    "a.txt": "",
 6781                    "b.txt": ""
 6782                },
 6783                "file1.txt": "",
 6784            },
 6785            "dir2": {
 6786                "subdir2": {
 6787                    "c.txt": "",
 6788                    "d.txt": ""
 6789                },
 6790                "file2.txt": "",
 6791            },
 6792            "file3.txt": "",
 6793        }),
 6794    )
 6795    .await;
 6796
 6797    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6798    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6799    let workspace = window
 6800        .read_with(cx, |mw, _| mw.workspace().clone())
 6801        .unwrap();
 6802    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6803    let panel = workspace.update_in(cx, ProjectPanel::new);
 6804    cx.run_until_parked();
 6805
 6806    toggle_expand_dir(&panel, "root/dir1", cx);
 6807    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 6808    toggle_expand_dir(&panel, "root/dir2", cx);
 6809    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
 6810
 6811    // Test Case 1: Select and delete nested directory with parent
 6812    cx.simulate_modifiers_change(gpui::Modifiers {
 6813        control: true,
 6814        ..Default::default()
 6815    });
 6816    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
 6817    select_path_with_mark(&panel, "root/dir1", cx);
 6818
 6819    assert_eq!(
 6820        visible_entries_as_strings(&panel, 0..15, cx),
 6821        &[
 6822            "v root",
 6823            "    v dir1  <== selected  <== marked",
 6824            "        v subdir1  <== marked",
 6825            "              a.txt",
 6826            "              b.txt",
 6827            "          file1.txt",
 6828            "    v dir2",
 6829            "        v subdir2",
 6830            "              c.txt",
 6831            "              d.txt",
 6832            "          file2.txt",
 6833            "      file3.txt",
 6834        ],
 6835        "Initial state before deleting nested directory with parent"
 6836    );
 6837
 6838    submit_deletion(&panel, cx);
 6839    assert_eq!(
 6840        visible_entries_as_strings(&panel, 0..15, cx),
 6841        &[
 6842            "v root",
 6843            "    v dir2  <== selected",
 6844            "        v subdir2",
 6845            "              c.txt",
 6846            "              d.txt",
 6847            "          file2.txt",
 6848            "      file3.txt",
 6849        ],
 6850        "Should select next directory after deleting directory with parent"
 6851    );
 6852
 6853    // Test Case 2: Select mixed files and directories across levels
 6854    select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
 6855    select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
 6856    select_path_with_mark(&panel, "root/file3.txt", cx);
 6857
 6858    assert_eq!(
 6859        visible_entries_as_strings(&panel, 0..15, cx),
 6860        &[
 6861            "v root",
 6862            "    v dir2",
 6863            "        v subdir2",
 6864            "              c.txt  <== marked",
 6865            "              d.txt",
 6866            "          file2.txt  <== marked",
 6867            "      file3.txt  <== selected  <== marked",
 6868        ],
 6869        "Initial state before deleting"
 6870    );
 6871
 6872    submit_deletion(&panel, cx);
 6873    assert_eq!(
 6874        visible_entries_as_strings(&panel, 0..15, cx),
 6875        &[
 6876            "v root",
 6877            "    v dir2  <== selected",
 6878            "        v subdir2",
 6879            "              d.txt",
 6880        ],
 6881        "Should select sibling directory"
 6882    );
 6883}
 6884
 6885#[gpui::test]
 6886async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
 6887    init_test_with_editor(cx);
 6888
 6889    let fs = FakeFs::new(cx.executor());
 6890    fs.insert_tree(
 6891        "/root",
 6892        json!({
 6893            "dir1": {
 6894                "subdir1": {
 6895                    "a.txt": "",
 6896                    "b.txt": ""
 6897                },
 6898                "file1.txt": "",
 6899            },
 6900            "dir2": {
 6901                "subdir2": {
 6902                    "c.txt": "",
 6903                    "d.txt": ""
 6904                },
 6905                "file2.txt": "",
 6906            },
 6907            "file3.txt": "",
 6908            "file4.txt": "",
 6909        }),
 6910    )
 6911    .await;
 6912
 6913    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6914    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6915    let workspace = window
 6916        .read_with(cx, |mw, _| mw.workspace().clone())
 6917        .unwrap();
 6918    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6919    let panel = workspace.update_in(cx, ProjectPanel::new);
 6920    cx.run_until_parked();
 6921
 6922    toggle_expand_dir(&panel, "root/dir1", cx);
 6923    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 6924    toggle_expand_dir(&panel, "root/dir2", cx);
 6925    toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
 6926
 6927    // Test Case 1: Select all root files and directories
 6928    cx.simulate_modifiers_change(gpui::Modifiers {
 6929        control: true,
 6930        ..Default::default()
 6931    });
 6932    select_path_with_mark(&panel, "root/dir1", cx);
 6933    select_path_with_mark(&panel, "root/dir2", cx);
 6934    select_path_with_mark(&panel, "root/file3.txt", cx);
 6935    select_path_with_mark(&panel, "root/file4.txt", cx);
 6936    assert_eq!(
 6937        visible_entries_as_strings(&panel, 0..20, cx),
 6938        &[
 6939            "v root",
 6940            "    v dir1  <== marked",
 6941            "        v subdir1",
 6942            "              a.txt",
 6943            "              b.txt",
 6944            "          file1.txt",
 6945            "    v dir2  <== marked",
 6946            "        v subdir2",
 6947            "              c.txt",
 6948            "              d.txt",
 6949            "          file2.txt",
 6950            "      file3.txt  <== marked",
 6951            "      file4.txt  <== selected  <== marked",
 6952        ],
 6953        "State before deleting all contents"
 6954    );
 6955
 6956    submit_deletion(&panel, cx);
 6957    assert_eq!(
 6958        visible_entries_as_strings(&panel, 0..20, cx),
 6959        &["v root  <== selected"],
 6960        "Only empty root directory should remain after deleting all contents"
 6961    );
 6962}
 6963
 6964#[gpui::test]
 6965async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
 6966    init_test_with_editor(cx);
 6967
 6968    let fs = FakeFs::new(cx.executor());
 6969    fs.insert_tree(
 6970        "/root",
 6971        json!({
 6972            "dir1": {
 6973                "subdir1": {
 6974                    "file_a.txt": "content a",
 6975                    "file_b.txt": "content b",
 6976                },
 6977                "subdir2": {
 6978                    "file_c.txt": "content c",
 6979                },
 6980                "file1.txt": "content 1",
 6981            },
 6982            "dir2": {
 6983                "file2.txt": "content 2",
 6984            },
 6985        }),
 6986    )
 6987    .await;
 6988
 6989    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 6990    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 6991    let workspace = window
 6992        .read_with(cx, |mw, _| mw.workspace().clone())
 6993        .unwrap();
 6994    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 6995    let panel = workspace.update_in(cx, ProjectPanel::new);
 6996    cx.run_until_parked();
 6997
 6998    toggle_expand_dir(&panel, "root/dir1", cx);
 6999    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7000    toggle_expand_dir(&panel, "root/dir2", cx);
 7001    cx.simulate_modifiers_change(gpui::Modifiers {
 7002        control: true,
 7003        ..Default::default()
 7004    });
 7005
 7006    // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
 7007    select_path_with_mark(&panel, "root/dir1", cx);
 7008    select_path_with_mark(&panel, "root/dir1/subdir1", cx);
 7009    select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
 7010
 7011    assert_eq!(
 7012        visible_entries_as_strings(&panel, 0..20, cx),
 7013        &[
 7014            "v root",
 7015            "    v dir1  <== marked",
 7016            "        v subdir1  <== marked",
 7017            "              file_a.txt  <== selected  <== marked",
 7018            "              file_b.txt",
 7019            "        > subdir2",
 7020            "          file1.txt",
 7021            "    v dir2",
 7022            "          file2.txt",
 7023        ],
 7024        "State with parent dir, subdir, and file selected"
 7025    );
 7026    submit_deletion(&panel, cx);
 7027    assert_eq!(
 7028        visible_entries_as_strings(&panel, 0..20, cx),
 7029        &["v root", "    v dir2  <== selected", "          file2.txt",],
 7030        "Only dir2 should remain after deletion"
 7031    );
 7032}
 7033
 7034#[gpui::test]
 7035async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
 7036    init_test_with_editor(cx);
 7037
 7038    let fs = FakeFs::new(cx.executor());
 7039    // First worktree
 7040    fs.insert_tree(
 7041        "/root1",
 7042        json!({
 7043            "dir1": {
 7044                "file1.txt": "content 1",
 7045                "file2.txt": "content 2",
 7046            },
 7047            "dir2": {
 7048                "file3.txt": "content 3",
 7049            },
 7050        }),
 7051    )
 7052    .await;
 7053
 7054    // Second worktree
 7055    fs.insert_tree(
 7056        "/root2",
 7057        json!({
 7058            "dir3": {
 7059                "file4.txt": "content 4",
 7060                "file5.txt": "content 5",
 7061            },
 7062            "file6.txt": "content 6",
 7063        }),
 7064    )
 7065    .await;
 7066
 7067    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 7068    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7069    let workspace = window
 7070        .read_with(cx, |mw, _| mw.workspace().clone())
 7071        .unwrap();
 7072    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7073    let panel = workspace.update_in(cx, ProjectPanel::new);
 7074    cx.run_until_parked();
 7075
 7076    // Expand all directories for testing
 7077    toggle_expand_dir(&panel, "root1/dir1", cx);
 7078    toggle_expand_dir(&panel, "root1/dir2", cx);
 7079    toggle_expand_dir(&panel, "root2/dir3", cx);
 7080
 7081    // Test Case 1: Delete files across different worktrees
 7082    cx.simulate_modifiers_change(gpui::Modifiers {
 7083        control: true,
 7084        ..Default::default()
 7085    });
 7086    select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
 7087    select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
 7088
 7089    assert_eq!(
 7090        visible_entries_as_strings(&panel, 0..20, cx),
 7091        &[
 7092            "v root1",
 7093            "    v dir1",
 7094            "          file1.txt  <== marked",
 7095            "          file2.txt",
 7096            "    v dir2",
 7097            "          file3.txt",
 7098            "v root2",
 7099            "    v dir3",
 7100            "          file4.txt  <== selected  <== marked",
 7101            "          file5.txt",
 7102            "      file6.txt",
 7103        ],
 7104        "Initial state with files selected from different worktrees"
 7105    );
 7106
 7107    submit_deletion(&panel, cx);
 7108    assert_eq!(
 7109        visible_entries_as_strings(&panel, 0..20, cx),
 7110        &[
 7111            "v root1",
 7112            "    v dir1",
 7113            "          file2.txt",
 7114            "    v dir2",
 7115            "          file3.txt",
 7116            "v root2",
 7117            "    v dir3",
 7118            "          file5.txt  <== selected",
 7119            "      file6.txt",
 7120        ],
 7121        "Should select next file in the last worktree after deletion"
 7122    );
 7123
 7124    // Test Case 2: Delete directories from different worktrees
 7125    select_path_with_mark(&panel, "root1/dir1", cx);
 7126    select_path_with_mark(&panel, "root2/dir3", cx);
 7127
 7128    assert_eq!(
 7129        visible_entries_as_strings(&panel, 0..20, cx),
 7130        &[
 7131            "v root1",
 7132            "    v dir1  <== marked",
 7133            "          file2.txt",
 7134            "    v dir2",
 7135            "          file3.txt",
 7136            "v root2",
 7137            "    v dir3  <== selected  <== marked",
 7138            "          file5.txt",
 7139            "      file6.txt",
 7140        ],
 7141        "State with directories marked from different worktrees"
 7142    );
 7143
 7144    submit_deletion(&panel, cx);
 7145    assert_eq!(
 7146        visible_entries_as_strings(&panel, 0..20, cx),
 7147        &[
 7148            "v root1",
 7149            "    v dir2",
 7150            "          file3.txt",
 7151            "v root2",
 7152            "      file6.txt  <== selected",
 7153        ],
 7154        "Should select remaining file in last worktree after directory deletion"
 7155    );
 7156
 7157    // Test Case 4: Delete all remaining files except roots
 7158    select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
 7159    select_path_with_mark(&panel, "root2/file6.txt", cx);
 7160
 7161    assert_eq!(
 7162        visible_entries_as_strings(&panel, 0..20, cx),
 7163        &[
 7164            "v root1",
 7165            "    v dir2",
 7166            "          file3.txt  <== marked",
 7167            "v root2",
 7168            "      file6.txt  <== selected  <== marked",
 7169        ],
 7170        "State with all remaining files marked"
 7171    );
 7172
 7173    submit_deletion(&panel, cx);
 7174    assert_eq!(
 7175        visible_entries_as_strings(&panel, 0..20, cx),
 7176        &["v root1", "    v dir2", "v root2  <== selected"],
 7177        "Second parent root should be selected after deleting"
 7178    );
 7179}
 7180
 7181#[gpui::test]
 7182async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
 7183    init_test_with_editor(cx);
 7184
 7185    let fs = FakeFs::new(cx.executor());
 7186    fs.insert_tree(
 7187        "/root",
 7188        json!({
 7189            "dir1": {
 7190                "file1.txt": "",
 7191                "file2.txt": "",
 7192                "file3.txt": "",
 7193            },
 7194            "dir2": {
 7195                "file4.txt": "",
 7196                "file5.txt": "",
 7197            },
 7198        }),
 7199    )
 7200    .await;
 7201
 7202    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 7203    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7204    let workspace = window
 7205        .read_with(cx, |mw, _| mw.workspace().clone())
 7206        .unwrap();
 7207    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7208    let panel = workspace.update_in(cx, ProjectPanel::new);
 7209    cx.run_until_parked();
 7210
 7211    toggle_expand_dir(&panel, "root/dir1", cx);
 7212    toggle_expand_dir(&panel, "root/dir2", cx);
 7213
 7214    cx.simulate_modifiers_change(gpui::Modifiers {
 7215        control: true,
 7216        ..Default::default()
 7217    });
 7218
 7219    select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
 7220    select_path(&panel, "root/dir1/file1.txt", cx);
 7221
 7222    assert_eq!(
 7223        visible_entries_as_strings(&panel, 0..15, cx),
 7224        &[
 7225            "v root",
 7226            "    v dir1",
 7227            "          file1.txt  <== selected",
 7228            "          file2.txt  <== marked",
 7229            "          file3.txt",
 7230            "    v dir2",
 7231            "          file4.txt",
 7232            "          file5.txt",
 7233        ],
 7234        "Initial state with one marked entry and different selection"
 7235    );
 7236
 7237    // Delete should operate on the selected entry (file1.txt)
 7238    submit_deletion(&panel, cx);
 7239    assert_eq!(
 7240        visible_entries_as_strings(&panel, 0..15, cx),
 7241        &[
 7242            "v root",
 7243            "    v dir1",
 7244            "          file2.txt  <== selected  <== marked",
 7245            "          file3.txt",
 7246            "    v dir2",
 7247            "          file4.txt",
 7248            "          file5.txt",
 7249        ],
 7250        "Should delete selected file, not marked file"
 7251    );
 7252
 7253    select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
 7254    select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
 7255    select_path(&panel, "root/dir2/file5.txt", cx);
 7256
 7257    assert_eq!(
 7258        visible_entries_as_strings(&panel, 0..15, cx),
 7259        &[
 7260            "v root",
 7261            "    v dir1",
 7262            "          file2.txt  <== marked",
 7263            "          file3.txt  <== marked",
 7264            "    v dir2",
 7265            "          file4.txt  <== marked",
 7266            "          file5.txt  <== selected",
 7267        ],
 7268        "Initial state with multiple marked entries and different selection"
 7269    );
 7270
 7271    // Delete should operate on all marked entries, ignoring the selection
 7272    submit_deletion(&panel, cx);
 7273    assert_eq!(
 7274        visible_entries_as_strings(&panel, 0..15, cx),
 7275        &[
 7276            "v root",
 7277            "    v dir1",
 7278            "    v dir2",
 7279            "          file5.txt  <== selected",
 7280        ],
 7281        "Should delete all marked files, leaving only the selected file"
 7282    );
 7283}
 7284
 7285#[gpui::test]
 7286async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
 7287    init_test_with_editor(cx);
 7288
 7289    let fs = FakeFs::new(cx.executor());
 7290    fs.insert_tree(
 7291        "/root_b",
 7292        json!({
 7293            "dir1": {
 7294                "file1.txt": "content 1",
 7295                "file2.txt": "content 2",
 7296            },
 7297        }),
 7298    )
 7299    .await;
 7300
 7301    fs.insert_tree(
 7302        "/root_c",
 7303        json!({
 7304            "dir2": {},
 7305        }),
 7306    )
 7307    .await;
 7308
 7309    let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
 7310    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7311    let workspace = window
 7312        .read_with(cx, |mw, _| mw.workspace().clone())
 7313        .unwrap();
 7314    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7315    let panel = workspace.update_in(cx, ProjectPanel::new);
 7316    cx.run_until_parked();
 7317
 7318    toggle_expand_dir(&panel, "root_b/dir1", cx);
 7319    toggle_expand_dir(&panel, "root_c/dir2", cx);
 7320
 7321    cx.simulate_modifiers_change(gpui::Modifiers {
 7322        control: true,
 7323        ..Default::default()
 7324    });
 7325    select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
 7326    select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
 7327
 7328    assert_eq!(
 7329        visible_entries_as_strings(&panel, 0..20, cx),
 7330        &[
 7331            "v root_b",
 7332            "    v dir1",
 7333            "          file1.txt  <== marked",
 7334            "          file2.txt  <== selected  <== marked",
 7335            "v root_c",
 7336            "    v dir2",
 7337        ],
 7338        "Initial state with files marked in root_b"
 7339    );
 7340
 7341    submit_deletion(&panel, cx);
 7342    assert_eq!(
 7343        visible_entries_as_strings(&panel, 0..20, cx),
 7344        &[
 7345            "v root_b",
 7346            "    v dir1  <== selected",
 7347            "v root_c",
 7348            "    v dir2",
 7349        ],
 7350        "After deletion in root_b as it's last deletion, selection should be in root_b"
 7351    );
 7352
 7353    select_path_with_mark(&panel, "root_c/dir2", cx);
 7354
 7355    submit_deletion(&panel, cx);
 7356    assert_eq!(
 7357        visible_entries_as_strings(&panel, 0..20, cx),
 7358        &["v root_b", "    v dir1", "v root_c  <== selected",],
 7359        "After deleting from root_c, it should remain in root_c"
 7360    );
 7361}
 7362
 7363fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 7364    let path = rel_path(path);
 7365    panel.update_in(cx, |panel, window, cx| {
 7366        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 7367            let worktree = worktree.read(cx);
 7368            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 7369                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 7370                panel.toggle_expanded(entry_id, window, cx);
 7371                return;
 7372            }
 7373        }
 7374        panic!("no worktree for path {:?}", path);
 7375    });
 7376    cx.run_until_parked();
 7377}
 7378
 7379#[gpui::test]
 7380async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
 7381    init_test_with_editor(cx);
 7382
 7383    let fs = FakeFs::new(cx.executor());
 7384    fs.insert_tree(
 7385        path!("/root"),
 7386        json!({
 7387            ".gitignore": "**/ignored_dir\n**/ignored_nested",
 7388            "dir1": {
 7389                "empty1": {
 7390                    "empty2": {
 7391                        "empty3": {
 7392                            "file.txt": ""
 7393                        }
 7394                    }
 7395                },
 7396                "subdir1": {
 7397                    "file1.txt": "",
 7398                    "file2.txt": "",
 7399                    "ignored_nested": {
 7400                        "ignored_file.txt": ""
 7401                    }
 7402                },
 7403                "ignored_dir": {
 7404                    "subdir": {
 7405                        "deep_file.txt": ""
 7406                    }
 7407                }
 7408            }
 7409        }),
 7410    )
 7411    .await;
 7412
 7413    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7414    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7415    let workspace = window
 7416        .read_with(cx, |mw, _| mw.workspace().clone())
 7417        .unwrap();
 7418    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7419
 7420    // Test 1: When auto-fold is enabled
 7421    cx.update(|_, cx| {
 7422        let settings = *ProjectPanelSettings::get_global(cx);
 7423        ProjectPanelSettings::override_global(
 7424            ProjectPanelSettings {
 7425                auto_fold_dirs: true,
 7426                ..settings
 7427            },
 7428            cx,
 7429        );
 7430    });
 7431
 7432    let panel = workspace.update_in(cx, ProjectPanel::new);
 7433    cx.run_until_parked();
 7434
 7435    assert_eq!(
 7436        visible_entries_as_strings(&panel, 0..20, cx),
 7437        &["v root", "    > dir1", "      .gitignore",],
 7438        "Initial state should show collapsed root structure"
 7439    );
 7440
 7441    toggle_expand_dir(&panel, "root/dir1", cx);
 7442    assert_eq!(
 7443        visible_entries_as_strings(&panel, 0..20, cx),
 7444        &[
 7445            "v root",
 7446            "    v dir1  <== selected",
 7447            "        > empty1/empty2/empty3",
 7448            "        > ignored_dir",
 7449            "        > subdir1",
 7450            "      .gitignore",
 7451        ],
 7452        "Should show first level with auto-folded dirs and ignored dir visible"
 7453    );
 7454
 7455    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7456    panel.update_in(cx, |panel, window, cx| {
 7457        let project = panel.project.read(cx);
 7458        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7459        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
 7460        panel.update_visible_entries(None, false, false, window, cx);
 7461    });
 7462    cx.run_until_parked();
 7463
 7464    assert_eq!(
 7465        visible_entries_as_strings(&panel, 0..20, cx),
 7466        &[
 7467            "v root",
 7468            "    v dir1  <== selected",
 7469            "        v empty1",
 7470            "            v empty2",
 7471            "                v empty3",
 7472            "                      file.txt",
 7473            "        > ignored_dir",
 7474            "        v subdir1",
 7475            "            > ignored_nested",
 7476            "              file1.txt",
 7477            "              file2.txt",
 7478            "      .gitignore",
 7479        ],
 7480        "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
 7481    );
 7482
 7483    // Test 2: When auto-fold is disabled
 7484    cx.update(|_, cx| {
 7485        let settings = *ProjectPanelSettings::get_global(cx);
 7486        ProjectPanelSettings::override_global(
 7487            ProjectPanelSettings {
 7488                auto_fold_dirs: false,
 7489                ..settings
 7490            },
 7491            cx,
 7492        );
 7493    });
 7494
 7495    panel.update_in(cx, |panel, window, cx| {
 7496        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
 7497    });
 7498
 7499    toggle_expand_dir(&panel, "root/dir1", cx);
 7500    assert_eq!(
 7501        visible_entries_as_strings(&panel, 0..20, cx),
 7502        &[
 7503            "v root",
 7504            "    v dir1  <== selected",
 7505            "        > empty1",
 7506            "        > ignored_dir",
 7507            "        > subdir1",
 7508            "      .gitignore",
 7509        ],
 7510        "With auto-fold disabled: should show all directories separately"
 7511    );
 7512
 7513    let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7514    panel.update_in(cx, |panel, window, cx| {
 7515        let project = panel.project.read(cx);
 7516        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7517        panel.expand_all_for_entry(worktree.id(), entry_id, cx);
 7518        panel.update_visible_entries(None, false, false, window, cx);
 7519    });
 7520    cx.run_until_parked();
 7521
 7522    assert_eq!(
 7523        visible_entries_as_strings(&panel, 0..20, cx),
 7524        &[
 7525            "v root",
 7526            "    v dir1  <== selected",
 7527            "        v empty1",
 7528            "            v empty2",
 7529            "                v empty3",
 7530            "                      file.txt",
 7531            "        > ignored_dir",
 7532            "        v subdir1",
 7533            "            > ignored_nested",
 7534            "              file1.txt",
 7535            "              file2.txt",
 7536            "      .gitignore",
 7537        ],
 7538        "After expand_all without auto-fold: should expand all dirs normally, \
 7539         expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
 7540    );
 7541
 7542    // Test 3: When explicitly called on ignored directory
 7543    let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
 7544    panel.update_in(cx, |panel, window, cx| {
 7545        let project = panel.project.read(cx);
 7546        let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7547        panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
 7548        panel.update_visible_entries(None, false, false, window, cx);
 7549    });
 7550    cx.run_until_parked();
 7551
 7552    assert_eq!(
 7553        visible_entries_as_strings(&panel, 0..20, cx),
 7554        &[
 7555            "v root",
 7556            "    v dir1  <== selected",
 7557            "        v empty1",
 7558            "            v empty2",
 7559            "                v empty3",
 7560            "                      file.txt",
 7561            "        v ignored_dir",
 7562            "            v subdir",
 7563            "                  deep_file.txt",
 7564            "        v subdir1",
 7565            "            > ignored_nested",
 7566            "              file1.txt",
 7567            "              file2.txt",
 7568            "      .gitignore",
 7569        ],
 7570        "After expand_all on ignored_dir: should expand all contents of the ignored directory"
 7571    );
 7572}
 7573
 7574#[gpui::test]
 7575async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
 7576    init_test(cx);
 7577
 7578    let fs = FakeFs::new(cx.executor());
 7579    fs.insert_tree(
 7580        path!("/root"),
 7581        json!({
 7582            "dir1": {
 7583                "subdir1": {
 7584                    "nested1": {
 7585                        "file1.txt": "",
 7586                        "file2.txt": ""
 7587                    },
 7588                },
 7589                "subdir2": {
 7590                    "file4.txt": ""
 7591                }
 7592            },
 7593            "dir2": {
 7594                "single_file": {
 7595                    "file5.txt": ""
 7596                }
 7597            }
 7598        }),
 7599    )
 7600    .await;
 7601
 7602    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7603    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7604    let workspace = window
 7605        .read_with(cx, |mw, _| mw.workspace().clone())
 7606        .unwrap();
 7607    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7608
 7609    // Test 1: Basic collapsing
 7610    {
 7611        let panel = workspace.update_in(cx, ProjectPanel::new);
 7612        cx.run_until_parked();
 7613
 7614        toggle_expand_dir(&panel, "root/dir1", cx);
 7615        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7616        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7617        toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
 7618
 7619        assert_eq!(
 7620            visible_entries_as_strings(&panel, 0..20, cx),
 7621            &[
 7622                "v root",
 7623                "    v dir1",
 7624                "        v subdir1",
 7625                "            v nested1",
 7626                "                  file1.txt",
 7627                "                  file2.txt",
 7628                "        v subdir2  <== selected",
 7629                "              file4.txt",
 7630                "    > dir2",
 7631            ],
 7632            "Initial state with everything expanded"
 7633        );
 7634
 7635        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7636        panel.update_in(cx, |panel, window, cx| {
 7637            let project = panel.project.read(cx);
 7638            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7639            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7640            panel.update_visible_entries(None, false, false, window, cx);
 7641        });
 7642        cx.run_until_parked();
 7643
 7644        assert_eq!(
 7645            visible_entries_as_strings(&panel, 0..20, cx),
 7646            &["v root", "    > dir1", "    > dir2",],
 7647            "All subdirs under dir1 should be collapsed"
 7648        );
 7649    }
 7650
 7651    // Test 2: With auto-fold enabled
 7652    {
 7653        cx.update(|_, cx| {
 7654            let settings = *ProjectPanelSettings::get_global(cx);
 7655            ProjectPanelSettings::override_global(
 7656                ProjectPanelSettings {
 7657                    auto_fold_dirs: true,
 7658                    ..settings
 7659                },
 7660                cx,
 7661            );
 7662        });
 7663
 7664        let panel = workspace.update_in(cx, ProjectPanel::new);
 7665        cx.run_until_parked();
 7666
 7667        toggle_expand_dir(&panel, "root/dir1", cx);
 7668        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7669        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7670
 7671        assert_eq!(
 7672            visible_entries_as_strings(&panel, 0..20, cx),
 7673            &[
 7674                "v root",
 7675                "    v dir1",
 7676                "        v subdir1/nested1  <== selected",
 7677                "              file1.txt",
 7678                "              file2.txt",
 7679                "        > subdir2",
 7680                "    > dir2/single_file",
 7681            ],
 7682            "Initial state with some dirs expanded"
 7683        );
 7684
 7685        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7686        panel.update(cx, |panel, cx| {
 7687            let project = panel.project.read(cx);
 7688            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7689            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7690        });
 7691
 7692        toggle_expand_dir(&panel, "root/dir1", cx);
 7693
 7694        assert_eq!(
 7695            visible_entries_as_strings(&panel, 0..20, cx),
 7696            &[
 7697                "v root",
 7698                "    v dir1  <== selected",
 7699                "        > subdir1/nested1",
 7700                "        > subdir2",
 7701                "    > dir2/single_file",
 7702            ],
 7703            "Subdirs should be collapsed and folded with auto-fold enabled"
 7704        );
 7705    }
 7706
 7707    // Test 3: With auto-fold disabled
 7708    {
 7709        cx.update(|_, cx| {
 7710            let settings = *ProjectPanelSettings::get_global(cx);
 7711            ProjectPanelSettings::override_global(
 7712                ProjectPanelSettings {
 7713                    auto_fold_dirs: false,
 7714                    ..settings
 7715                },
 7716                cx,
 7717            );
 7718        });
 7719
 7720        let panel = workspace.update_in(cx, ProjectPanel::new);
 7721        cx.run_until_parked();
 7722
 7723        toggle_expand_dir(&panel, "root/dir1", cx);
 7724        toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7725        toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7726
 7727        assert_eq!(
 7728            visible_entries_as_strings(&panel, 0..20, cx),
 7729            &[
 7730                "v root",
 7731                "    v dir1",
 7732                "        v subdir1",
 7733                "            v nested1  <== selected",
 7734                "                  file1.txt",
 7735                "                  file2.txt",
 7736                "        > subdir2",
 7737                "    > dir2",
 7738            ],
 7739            "Initial state with some dirs expanded and auto-fold disabled"
 7740        );
 7741
 7742        let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
 7743        panel.update(cx, |panel, cx| {
 7744            let project = panel.project.read(cx);
 7745            let worktree = project.worktrees(cx).next().unwrap().read(cx);
 7746            panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
 7747        });
 7748
 7749        toggle_expand_dir(&panel, "root/dir1", cx);
 7750
 7751        assert_eq!(
 7752            visible_entries_as_strings(&panel, 0..20, cx),
 7753            &[
 7754                "v root",
 7755                "    v dir1  <== selected",
 7756                "        > subdir1",
 7757                "        > subdir2",
 7758                "    > dir2",
 7759            ],
 7760            "Subdirs should be collapsed but not folded with auto-fold disabled"
 7761        );
 7762    }
 7763}
 7764
 7765#[gpui::test]
 7766async fn test_collapse_selected_entry_and_children_action(cx: &mut gpui::TestAppContext) {
 7767    init_test(cx);
 7768
 7769    let fs = FakeFs::new(cx.executor());
 7770    fs.insert_tree(
 7771        path!("/root"),
 7772        json!({
 7773            "dir1": {
 7774                "subdir1": {
 7775                    "nested1": {
 7776                        "file1.txt": "",
 7777                        "file2.txt": ""
 7778                    },
 7779                },
 7780                "subdir2": {
 7781                    "file3.txt": ""
 7782                }
 7783            },
 7784            "dir2": {
 7785                "file4.txt": ""
 7786            }
 7787        }),
 7788    )
 7789    .await;
 7790
 7791    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7792    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7793    let workspace = window
 7794        .read_with(cx, |mw, _| mw.workspace().clone())
 7795        .unwrap();
 7796    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7797
 7798    let panel = workspace.update_in(cx, ProjectPanel::new);
 7799    cx.run_until_parked();
 7800
 7801    toggle_expand_dir(&panel, "root/dir1", cx);
 7802    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7803    toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
 7804    toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
 7805    toggle_expand_dir(&panel, "root/dir2", cx);
 7806
 7807    assert_eq!(
 7808        visible_entries_as_strings(&panel, 0..20, cx),
 7809        &[
 7810            "v root",
 7811            "    v dir1",
 7812            "        v subdir1",
 7813            "            v nested1",
 7814            "                  file1.txt",
 7815            "                  file2.txt",
 7816            "        v subdir2",
 7817            "              file3.txt",
 7818            "    v dir2  <== selected",
 7819            "          file4.txt",
 7820        ],
 7821        "Initial state with directories expanded"
 7822    );
 7823
 7824    select_path(&panel, "root/dir1", cx);
 7825    cx.run_until_parked();
 7826
 7827    panel.update_in(cx, |panel, window, cx| {
 7828        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7829    });
 7830    cx.run_until_parked();
 7831
 7832    assert_eq!(
 7833        visible_entries_as_strings(&panel, 0..20, cx),
 7834        &[
 7835            "v root",
 7836            "    > dir1  <== selected",
 7837            "    v dir2",
 7838            "          file4.txt",
 7839        ],
 7840        "dir1 and all its children should be collapsed, dir2 should remain expanded"
 7841    );
 7842
 7843    toggle_expand_dir(&panel, "root/dir1", cx);
 7844    cx.run_until_parked();
 7845
 7846    assert_eq!(
 7847        visible_entries_as_strings(&panel, 0..20, cx),
 7848        &[
 7849            "v root",
 7850            "    v dir1  <== selected",
 7851            "        > subdir1",
 7852            "        > subdir2",
 7853            "    v dir2",
 7854            "          file4.txt",
 7855        ],
 7856        "After re-expanding dir1, its children should still be collapsed"
 7857    );
 7858}
 7859
 7860#[gpui::test]
 7861async fn test_collapse_root_single_worktree(cx: &mut gpui::TestAppContext) {
 7862    init_test(cx);
 7863
 7864    let fs = FakeFs::new(cx.executor());
 7865    fs.insert_tree(
 7866        path!("/root"),
 7867        json!({
 7868            "dir1": {
 7869                "subdir1": {
 7870                    "file1.txt": ""
 7871                },
 7872                "file2.txt": ""
 7873            },
 7874            "dir2": {
 7875                "file3.txt": ""
 7876            }
 7877        }),
 7878    )
 7879    .await;
 7880
 7881    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 7882    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7883    let workspace = window
 7884        .read_with(cx, |mw, _| mw.workspace().clone())
 7885        .unwrap();
 7886    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7887
 7888    let panel = workspace.update_in(cx, ProjectPanel::new);
 7889    cx.run_until_parked();
 7890
 7891    toggle_expand_dir(&panel, "root/dir1", cx);
 7892    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 7893    toggle_expand_dir(&panel, "root/dir2", cx);
 7894
 7895    assert_eq!(
 7896        visible_entries_as_strings(&panel, 0..20, cx),
 7897        &[
 7898            "v root",
 7899            "    v dir1",
 7900            "        v subdir1",
 7901            "              file1.txt",
 7902            "          file2.txt",
 7903            "    v dir2  <== selected",
 7904            "          file3.txt",
 7905        ],
 7906        "Initial state with directories expanded"
 7907    );
 7908
 7909    // Select the root and collapse it and its children
 7910    select_path(&panel, "root", cx);
 7911    cx.run_until_parked();
 7912
 7913    panel.update_in(cx, |panel, window, cx| {
 7914        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 7915    });
 7916    cx.run_until_parked();
 7917
 7918    // The root and all its children should be collapsed
 7919    assert_eq!(
 7920        visible_entries_as_strings(&panel, 0..20, cx),
 7921        &["> root  <== selected"],
 7922        "Root and all children should be collapsed"
 7923    );
 7924
 7925    // Re-expand root and dir1, verify children were recursively collapsed
 7926    toggle_expand_dir(&panel, "root", cx);
 7927    toggle_expand_dir(&panel, "root/dir1", cx);
 7928    cx.run_until_parked();
 7929
 7930    assert_eq!(
 7931        visible_entries_as_strings(&panel, 0..20, cx),
 7932        &[
 7933            "v root",
 7934            "    v dir1  <== selected",
 7935            "        > subdir1",
 7936            "          file2.txt",
 7937            "    > dir2",
 7938        ],
 7939        "After re-expanding root and dir1, subdir1 should still be collapsed"
 7940    );
 7941}
 7942
 7943#[gpui::test]
 7944async fn test_collapse_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 7945    init_test(cx);
 7946
 7947    let fs = FakeFs::new(cx.executor());
 7948    fs.insert_tree(
 7949        path!("/root1"),
 7950        json!({
 7951            "dir1": {
 7952                "subdir1": {
 7953                    "file1.txt": ""
 7954                },
 7955                "file2.txt": ""
 7956            }
 7957        }),
 7958    )
 7959    .await;
 7960    fs.insert_tree(
 7961        path!("/root2"),
 7962        json!({
 7963            "dir2": {
 7964                "file3.txt": ""
 7965            },
 7966            "file4.txt": ""
 7967        }),
 7968    )
 7969    .await;
 7970
 7971    let project = Project::test(
 7972        fs.clone(),
 7973        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 7974        cx,
 7975    )
 7976    .await;
 7977    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 7978    let workspace = window
 7979        .read_with(cx, |mw, _| mw.workspace().clone())
 7980        .unwrap();
 7981    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 7982
 7983    let panel = workspace.update_in(cx, ProjectPanel::new);
 7984    cx.run_until_parked();
 7985
 7986    toggle_expand_dir(&panel, "root1/dir1", cx);
 7987    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 7988    toggle_expand_dir(&panel, "root2/dir2", cx);
 7989
 7990    assert_eq!(
 7991        visible_entries_as_strings(&panel, 0..20, cx),
 7992        &[
 7993            "v root1",
 7994            "    v dir1",
 7995            "        v subdir1",
 7996            "              file1.txt",
 7997            "          file2.txt",
 7998            "v root2",
 7999            "    v dir2  <== selected",
 8000            "          file3.txt",
 8001            "      file4.txt",
 8002        ],
 8003        "Initial state with directories expanded across worktrees"
 8004    );
 8005
 8006    // Select root1 and collapse it and its children.
 8007    // In a multi-worktree project, this should only collapse the selected worktree,
 8008    // leaving other worktrees unaffected.
 8009    select_path(&panel, "root1", cx);
 8010    cx.run_until_parked();
 8011
 8012    panel.update_in(cx, |panel, window, cx| {
 8013        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 8014    });
 8015    cx.run_until_parked();
 8016
 8017    assert_eq!(
 8018        visible_entries_as_strings(&panel, 0..20, cx),
 8019        &[
 8020            "> root1  <== selected",
 8021            "v root2",
 8022            "    v dir2",
 8023            "          file3.txt",
 8024            "      file4.txt",
 8025        ],
 8026        "Only root1 should be collapsed, root2 should remain expanded"
 8027    );
 8028
 8029    // Re-expand root1 and verify its children were recursively collapsed
 8030    toggle_expand_dir(&panel, "root1", cx);
 8031
 8032    assert_eq!(
 8033        visible_entries_as_strings(&panel, 0..20, cx),
 8034        &[
 8035            "v root1  <== selected",
 8036            "    > dir1",
 8037            "v root2",
 8038            "    v dir2",
 8039            "          file3.txt",
 8040            "      file4.txt",
 8041        ],
 8042        "After re-expanding root1, dir1 should still be collapsed, root2 should be unaffected"
 8043    );
 8044}
 8045
 8046#[gpui::test]
 8047async fn test_collapse_non_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 8048    init_test(cx);
 8049
 8050    let fs = FakeFs::new(cx.executor());
 8051    fs.insert_tree(
 8052        path!("/root1"),
 8053        json!({
 8054            "dir1": {
 8055                "subdir1": {
 8056                    "file1.txt": ""
 8057                },
 8058                "file2.txt": ""
 8059            }
 8060        }),
 8061    )
 8062    .await;
 8063    fs.insert_tree(
 8064        path!("/root2"),
 8065        json!({
 8066            "dir2": {
 8067                "subdir2": {
 8068                    "file3.txt": ""
 8069                },
 8070                "file4.txt": ""
 8071            }
 8072        }),
 8073    )
 8074    .await;
 8075
 8076    let project = Project::test(
 8077        fs.clone(),
 8078        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 8079        cx,
 8080    )
 8081    .await;
 8082    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8083    let workspace = window
 8084        .read_with(cx, |mw, _| mw.workspace().clone())
 8085        .unwrap();
 8086    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8087
 8088    let panel = workspace.update_in(cx, ProjectPanel::new);
 8089    cx.run_until_parked();
 8090
 8091    toggle_expand_dir(&panel, "root1/dir1", cx);
 8092    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 8093    toggle_expand_dir(&panel, "root2/dir2", cx);
 8094    toggle_expand_dir(&panel, "root2/dir2/subdir2", cx);
 8095
 8096    assert_eq!(
 8097        visible_entries_as_strings(&panel, 0..20, cx),
 8098        &[
 8099            "v root1",
 8100            "    v dir1",
 8101            "        v subdir1",
 8102            "              file1.txt",
 8103            "          file2.txt",
 8104            "v root2",
 8105            "    v dir2",
 8106            "        v subdir2  <== selected",
 8107            "              file3.txt",
 8108            "          file4.txt",
 8109        ],
 8110        "Initial state with directories expanded across worktrees"
 8111    );
 8112
 8113    // Select dir1 in root1 and collapse it
 8114    select_path(&panel, "root1/dir1", cx);
 8115    cx.run_until_parked();
 8116
 8117    panel.update_in(cx, |panel, window, cx| {
 8118        panel.collapse_selected_entry_and_children(&CollapseSelectedEntryAndChildren, window, cx);
 8119    });
 8120    cx.run_until_parked();
 8121
 8122    assert_eq!(
 8123        visible_entries_as_strings(&panel, 0..20, cx),
 8124        &[
 8125            "v root1",
 8126            "    > dir1  <== selected",
 8127            "v root2",
 8128            "    v dir2",
 8129            "        v subdir2",
 8130            "              file3.txt",
 8131            "          file4.txt",
 8132        ],
 8133        "Only dir1 should be collapsed, root2 should be completely unaffected"
 8134    );
 8135
 8136    // Re-expand dir1 and verify subdir1 was recursively collapsed
 8137    toggle_expand_dir(&panel, "root1/dir1", cx);
 8138
 8139    assert_eq!(
 8140        visible_entries_as_strings(&panel, 0..20, cx),
 8141        &[
 8142            "v root1",
 8143            "    v dir1  <== selected",
 8144            "        > subdir1",
 8145            "          file2.txt",
 8146            "v root2",
 8147            "    v dir2",
 8148            "        v subdir2",
 8149            "              file3.txt",
 8150            "          file4.txt",
 8151        ],
 8152        "After re-expanding dir1, subdir1 should still be collapsed"
 8153    );
 8154}
 8155
 8156#[gpui::test]
 8157async fn test_collapse_all_for_root_single_worktree(cx: &mut gpui::TestAppContext) {
 8158    init_test(cx);
 8159
 8160    let fs = FakeFs::new(cx.executor());
 8161    fs.insert_tree(
 8162        path!("/root"),
 8163        json!({
 8164            "dir1": {
 8165                "subdir1": {
 8166                    "file1.txt": ""
 8167                },
 8168                "file2.txt": ""
 8169            },
 8170            "dir2": {
 8171                "file3.txt": ""
 8172            }
 8173        }),
 8174    )
 8175    .await;
 8176
 8177    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8178    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8179    let workspace = window
 8180        .read_with(cx, |mw, _| mw.workspace().clone())
 8181        .unwrap();
 8182    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8183
 8184    let panel = workspace.update_in(cx, ProjectPanel::new);
 8185    cx.run_until_parked();
 8186
 8187    toggle_expand_dir(&panel, "root/dir1", cx);
 8188    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 8189    toggle_expand_dir(&panel, "root/dir2", cx);
 8190
 8191    assert_eq!(
 8192        visible_entries_as_strings(&panel, 0..20, cx),
 8193        &[
 8194            "v root",
 8195            "    v dir1",
 8196            "        v subdir1",
 8197            "              file1.txt",
 8198            "          file2.txt",
 8199            "    v dir2  <== selected",
 8200            "          file3.txt",
 8201        ],
 8202        "Initial state with directories expanded"
 8203    );
 8204
 8205    select_path(&panel, "root", cx);
 8206    cx.run_until_parked();
 8207
 8208    panel.update_in(cx, |panel, window, cx| {
 8209        panel.collapse_all_for_root(window, cx);
 8210    });
 8211    cx.run_until_parked();
 8212
 8213    assert_eq!(
 8214        visible_entries_as_strings(&panel, 0..20, cx),
 8215        &["v root  <== selected", "    > dir1", "    > dir2"],
 8216        "Root should remain expanded but all children should be collapsed"
 8217    );
 8218
 8219    toggle_expand_dir(&panel, "root/dir1", cx);
 8220    cx.run_until_parked();
 8221
 8222    assert_eq!(
 8223        visible_entries_as_strings(&panel, 0..20, cx),
 8224        &[
 8225            "v root",
 8226            "    v dir1  <== selected",
 8227            "        > subdir1",
 8228            "          file2.txt",
 8229            "    > dir2",
 8230        ],
 8231        "After re-expanding dir1, subdir1 should still be collapsed"
 8232    );
 8233}
 8234
 8235#[gpui::test]
 8236async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext) {
 8237    init_test(cx);
 8238
 8239    let fs = FakeFs::new(cx.executor());
 8240    fs.insert_tree(
 8241        path!("/root1"),
 8242        json!({
 8243            "dir1": {
 8244                "subdir1": {
 8245                    "file1.txt": ""
 8246                },
 8247                "file2.txt": ""
 8248            }
 8249        }),
 8250    )
 8251    .await;
 8252    fs.insert_tree(
 8253        path!("/root2"),
 8254        json!({
 8255            "dir2": {
 8256                "file3.txt": ""
 8257            },
 8258            "file4.txt": ""
 8259        }),
 8260    )
 8261    .await;
 8262
 8263    let project = Project::test(
 8264        fs.clone(),
 8265        [path!("/root1").as_ref(), path!("/root2").as_ref()],
 8266        cx,
 8267    )
 8268    .await;
 8269    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8270    let workspace = window
 8271        .read_with(cx, |mw, _| mw.workspace().clone())
 8272        .unwrap();
 8273    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8274
 8275    let panel = workspace.update_in(cx, ProjectPanel::new);
 8276    cx.run_until_parked();
 8277
 8278    toggle_expand_dir(&panel, "root1/dir1", cx);
 8279    toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
 8280    toggle_expand_dir(&panel, "root2/dir2", cx);
 8281
 8282    assert_eq!(
 8283        visible_entries_as_strings(&panel, 0..20, cx),
 8284        &[
 8285            "v root1",
 8286            "    v dir1",
 8287            "        v subdir1",
 8288            "              file1.txt",
 8289            "          file2.txt",
 8290            "v root2",
 8291            "    v dir2  <== selected",
 8292            "          file3.txt",
 8293            "      file4.txt",
 8294        ],
 8295        "Initial state with directories expanded across worktrees"
 8296    );
 8297
 8298    select_path(&panel, "root1", cx);
 8299    cx.run_until_parked();
 8300
 8301    panel.update_in(cx, |panel, window, cx| {
 8302        panel.collapse_all_for_root(window, cx);
 8303    });
 8304    cx.run_until_parked();
 8305
 8306    assert_eq!(
 8307        visible_entries_as_strings(&panel, 0..20, cx),
 8308        &[
 8309            "> root1  <== selected",
 8310            "v root2",
 8311            "    v dir2",
 8312            "          file3.txt",
 8313            "      file4.txt",
 8314        ],
 8315        "With multiple worktrees, root1 should collapse completely (including itself)"
 8316    );
 8317}
 8318
 8319#[gpui::test]
 8320async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppContext) {
 8321    init_test(cx);
 8322
 8323    let fs = FakeFs::new(cx.executor());
 8324    fs.insert_tree(
 8325        path!("/root"),
 8326        json!({
 8327            "dir1": {
 8328                "subdir1": {
 8329                    "file1.txt": ""
 8330                },
 8331            },
 8332            "dir2": {
 8333                "file2.txt": ""
 8334            }
 8335        }),
 8336    )
 8337    .await;
 8338
 8339    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8340    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8341    let workspace = window
 8342        .read_with(cx, |mw, _| mw.workspace().clone())
 8343        .unwrap();
 8344    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8345
 8346    let panel = workspace.update_in(cx, ProjectPanel::new);
 8347    cx.run_until_parked();
 8348
 8349    toggle_expand_dir(&panel, "root/dir1", cx);
 8350    toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
 8351    toggle_expand_dir(&panel, "root/dir2", cx);
 8352
 8353    assert_eq!(
 8354        visible_entries_as_strings(&panel, 0..20, cx),
 8355        &[
 8356            "v root",
 8357            "    v dir1",
 8358            "        v subdir1",
 8359            "              file1.txt",
 8360            "    v dir2  <== selected",
 8361            "          file2.txt",
 8362        ],
 8363        "Initial state with directories expanded"
 8364    );
 8365
 8366    select_path(&panel, "root/dir1", cx);
 8367    cx.run_until_parked();
 8368
 8369    panel.update_in(cx, |panel, window, cx| {
 8370        panel.collapse_all_for_root(window, cx);
 8371    });
 8372    cx.run_until_parked();
 8373
 8374    assert_eq!(
 8375        visible_entries_as_strings(&panel, 0..20, cx),
 8376        &[
 8377            "v root",
 8378            "    v dir1  <== selected",
 8379            "        v subdir1",
 8380            "              file1.txt",
 8381            "    v dir2",
 8382            "          file2.txt",
 8383        ],
 8384        "collapse_all_for_root should be a no-op when called on a non-root directory"
 8385    );
 8386}
 8387
 8388#[gpui::test]
 8389async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
 8390    init_test(cx);
 8391
 8392    let fs = FakeFs::new(cx.executor());
 8393    fs.insert_tree(
 8394        path!("/root"),
 8395        json!({
 8396            "dir1": {
 8397                "file1.txt": "",
 8398            },
 8399        }),
 8400    )
 8401    .await;
 8402
 8403    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8404    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8405    let workspace = window
 8406        .read_with(cx, |mw, _| mw.workspace().clone())
 8407        .unwrap();
 8408    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8409
 8410    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8411        let panel = ProjectPanel::new(workspace, window, cx);
 8412        workspace.add_panel(panel.clone(), window, cx);
 8413        panel
 8414    });
 8415    cx.run_until_parked();
 8416
 8417    #[rustfmt::skip]
 8418    assert_eq!(
 8419        visible_entries_as_strings(&panel, 0..20, cx),
 8420        &[
 8421            "v root",
 8422            "    > dir1",
 8423        ],
 8424        "Initial state with nothing selected"
 8425    );
 8426
 8427    panel.update_in(cx, |panel, window, cx| {
 8428        panel.new_file(&NewFile, window, cx);
 8429    });
 8430    cx.run_until_parked();
 8431    panel.update_in(cx, |panel, window, cx| {
 8432        assert!(panel.filename_editor.read(cx).is_focused(window));
 8433    });
 8434    panel
 8435        .update_in(cx, |panel, window, cx| {
 8436            panel.filename_editor.update(cx, |editor, cx| {
 8437                editor.set_text("hello_from_no_selections", window, cx)
 8438            });
 8439            panel.confirm_edit(true, window, cx).unwrap()
 8440        })
 8441        .await
 8442        .unwrap();
 8443    cx.run_until_parked();
 8444    #[rustfmt::skip]
 8445    assert_eq!(
 8446        visible_entries_as_strings(&panel, 0..20, cx),
 8447        &[
 8448            "v root",
 8449            "    > dir1",
 8450            "      hello_from_no_selections  <== selected  <== marked",
 8451        ],
 8452        "A new file is created under the root directory"
 8453    );
 8454}
 8455
 8456#[gpui::test]
 8457async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
 8458    init_test(cx);
 8459
 8460    let fs = FakeFs::new(cx.executor());
 8461    fs.insert_tree(
 8462        path!("/root"),
 8463        json!({
 8464            "existing_dir": {
 8465                "existing_file.txt": "",
 8466            },
 8467            "existing_file.txt": "",
 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    cx.update(|_, cx| {
 8480        let settings = *ProjectPanelSettings::get_global(cx);
 8481        ProjectPanelSettings::override_global(
 8482            ProjectPanelSettings {
 8483                hide_root: true,
 8484                ..settings
 8485            },
 8486            cx,
 8487        );
 8488    });
 8489
 8490    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8491        let panel = ProjectPanel::new(workspace, window, cx);
 8492        workspace.add_panel(panel.clone(), window, cx);
 8493        panel
 8494    });
 8495    cx.run_until_parked();
 8496
 8497    #[rustfmt::skip]
 8498    assert_eq!(
 8499        visible_entries_as_strings(&panel, 0..20, cx),
 8500        &[
 8501            "> existing_dir",
 8502            "  existing_file.txt",
 8503        ],
 8504        "Initial state with hide_root=true, root should be hidden and nothing selected"
 8505    );
 8506
 8507    panel.update(cx, |panel, _| {
 8508        assert!(
 8509            panel.selection.is_none(),
 8510            "Should have no selection initially"
 8511        );
 8512    });
 8513
 8514    // Test 1: Create new file when no entry is selected
 8515    panel.update_in(cx, |panel, window, cx| {
 8516        panel.new_file(&NewFile, window, cx);
 8517    });
 8518    cx.run_until_parked();
 8519    panel.update_in(cx, |panel, window, cx| {
 8520        assert!(panel.filename_editor.read(cx).is_focused(window));
 8521    });
 8522    cx.run_until_parked();
 8523    #[rustfmt::skip]
 8524    assert_eq!(
 8525        visible_entries_as_strings(&panel, 0..20, cx),
 8526        &[
 8527            "> existing_dir",
 8528            "  [EDITOR: '']  <== selected",
 8529            "  existing_file.txt",
 8530        ],
 8531        "Editor should appear at root level when hide_root=true and no selection"
 8532    );
 8533
 8534    let confirm = panel.update_in(cx, |panel, window, cx| {
 8535        panel.filename_editor.update(cx, |editor, cx| {
 8536            editor.set_text("new_file_at_root.txt", window, cx)
 8537        });
 8538        panel.confirm_edit(true, window, cx).unwrap()
 8539    });
 8540    confirm.await.unwrap();
 8541    cx.run_until_parked();
 8542
 8543    #[rustfmt::skip]
 8544    assert_eq!(
 8545        visible_entries_as_strings(&panel, 0..20, cx),
 8546        &[
 8547            "> existing_dir",
 8548            "  existing_file.txt",
 8549            "  new_file_at_root.txt  <== selected  <== marked",
 8550        ],
 8551        "New file should be created at root level and visible without root prefix"
 8552    );
 8553
 8554    assert!(
 8555        fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
 8556        "File should be created in the actual root directory"
 8557    );
 8558
 8559    // Test 2: Create new directory when no entry is selected
 8560    panel.update(cx, |panel, _| {
 8561        panel.selection = None;
 8562    });
 8563
 8564    panel.update_in(cx, |panel, window, cx| {
 8565        panel.new_directory(&NewDirectory, window, cx);
 8566    });
 8567    cx.run_until_parked();
 8568
 8569    panel.update_in(cx, |panel, window, cx| {
 8570        assert!(panel.filename_editor.read(cx).is_focused(window));
 8571    });
 8572
 8573    #[rustfmt::skip]
 8574    assert_eq!(
 8575        visible_entries_as_strings(&panel, 0..20, cx),
 8576        &[
 8577            "> [EDITOR: '']  <== selected",
 8578            "> existing_dir",
 8579            "  existing_file.txt",
 8580            "  new_file_at_root.txt",
 8581        ],
 8582        "Directory editor should appear at root level when hide_root=true and no selection"
 8583    );
 8584
 8585    let confirm = panel.update_in(cx, |panel, window, cx| {
 8586        panel.filename_editor.update(cx, |editor, cx| {
 8587            editor.set_text("new_dir_at_root", window, cx)
 8588        });
 8589        panel.confirm_edit(true, window, cx).unwrap()
 8590    });
 8591    confirm.await.unwrap();
 8592    cx.run_until_parked();
 8593
 8594    #[rustfmt::skip]
 8595    assert_eq!(
 8596        visible_entries_as_strings(&panel, 0..20, cx),
 8597        &[
 8598            "> existing_dir",
 8599            "v new_dir_at_root  <== selected",
 8600            "  existing_file.txt",
 8601            "  new_file_at_root.txt",
 8602        ],
 8603        "New directory should be created at root level and visible without root prefix"
 8604    );
 8605
 8606    assert!(
 8607        fs.is_dir(Path::new("/root/new_dir_at_root")).await,
 8608        "Directory should be created in the actual root directory"
 8609    );
 8610}
 8611
 8612#[cfg(windows)]
 8613#[gpui::test]
 8614async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) {
 8615    init_test(cx);
 8616
 8617    let fs = FakeFs::new(cx.executor());
 8618    fs.insert_tree(
 8619        path!("/root"),
 8620        json!({
 8621            "dir1": {
 8622                "file1.txt": "",
 8623            },
 8624        }),
 8625    )
 8626    .await;
 8627
 8628    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8629    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8630    let workspace = window
 8631        .read_with(cx, |mw, _| mw.workspace().clone())
 8632        .unwrap();
 8633    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8634
 8635    let panel = workspace.update_in(cx, |workspace, window, cx| {
 8636        let panel = ProjectPanel::new(workspace, window, cx);
 8637        workspace.add_panel(panel.clone(), window, cx);
 8638        panel
 8639    });
 8640    cx.run_until_parked();
 8641
 8642    #[rustfmt::skip]
 8643    assert_eq!(
 8644        visible_entries_as_strings(&panel, 0..20, cx),
 8645        &[
 8646            "v root",
 8647            "    > dir1",
 8648        ],
 8649        "Initial state with nothing selected"
 8650    );
 8651
 8652    panel.update_in(cx, |panel, window, cx| {
 8653        panel.new_file(&NewFile, window, cx);
 8654    });
 8655    cx.run_until_parked();
 8656    panel.update_in(cx, |panel, window, cx| {
 8657        assert!(panel.filename_editor.read(cx).is_focused(window));
 8658    });
 8659    panel
 8660        .update_in(cx, |panel, window, cx| {
 8661            panel
 8662                .filename_editor
 8663                .update(cx, |editor, cx| editor.set_text("foo.", window, cx));
 8664            panel.confirm_edit(true, window, cx).unwrap()
 8665        })
 8666        .await
 8667        .unwrap();
 8668    cx.run_until_parked();
 8669    #[rustfmt::skip]
 8670    assert_eq!(
 8671        visible_entries_as_strings(&panel, 0..20, cx),
 8672        &[
 8673            "v root",
 8674            "    > dir1",
 8675            "      foo  <== selected  <== marked",
 8676        ],
 8677        "A new file is created under the root directory without the trailing dot"
 8678    );
 8679}
 8680
 8681#[gpui::test]
 8682async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
 8683    init_test(cx);
 8684
 8685    let fs = FakeFs::new(cx.executor());
 8686    fs.insert_tree(
 8687        "/root",
 8688        json!({
 8689            "dir1": {
 8690                "file1.txt": "",
 8691                "dir2": {
 8692                    "file2.txt": ""
 8693                }
 8694            },
 8695            "file3.txt": ""
 8696        }),
 8697    )
 8698    .await;
 8699
 8700    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 8701    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8702    let workspace = window
 8703        .read_with(cx, |mw, _| mw.workspace().clone())
 8704        .unwrap();
 8705    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8706    let panel = workspace.update_in(cx, ProjectPanel::new);
 8707    cx.run_until_parked();
 8708
 8709    panel.update(cx, |panel, cx| {
 8710        let project = panel.project.read(cx);
 8711        let worktree = project.visible_worktrees(cx).next().unwrap();
 8712        let worktree = worktree.read(cx);
 8713
 8714        // Test 1: Target is a directory, should highlight the directory itself
 8715        let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
 8716        let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
 8717        assert_eq!(
 8718            result,
 8719            Some(dir_entry.id),
 8720            "Should highlight directory itself"
 8721        );
 8722
 8723        // Test 2: Target is nested file, should highlight immediate parent
 8724        let nested_file = worktree
 8725            .entry_for_path(rel_path("dir1/dir2/file2.txt"))
 8726            .unwrap();
 8727        let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
 8728        let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
 8729        assert_eq!(
 8730            result,
 8731            Some(nested_parent.id),
 8732            "Should highlight immediate parent"
 8733        );
 8734
 8735        // Test 3: Target is root level file, should highlight root
 8736        let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
 8737        let result = panel.highlight_entry_for_external_drag(root_file, worktree);
 8738        assert_eq!(
 8739            result,
 8740            Some(worktree.root_entry().unwrap().id),
 8741            "Root level file should return None"
 8742        );
 8743
 8744        // Test 4: Target is root itself, should highlight root
 8745        let root_entry = worktree.root_entry().unwrap();
 8746        let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
 8747        assert_eq!(
 8748            result,
 8749            Some(root_entry.id),
 8750            "Root level file should return None"
 8751        );
 8752    });
 8753}
 8754
 8755#[gpui::test]
 8756async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
 8757    init_test(cx);
 8758
 8759    let fs = FakeFs::new(cx.executor());
 8760    fs.insert_tree(
 8761        "/root",
 8762        json!({
 8763            "parent_dir": {
 8764                "child_file.txt": "",
 8765                "sibling_file.txt": "",
 8766                "child_dir": {
 8767                    "nested_file.txt": ""
 8768                }
 8769            },
 8770            "other_dir": {
 8771                "other_file.txt": ""
 8772            }
 8773        }),
 8774    )
 8775    .await;
 8776
 8777    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 8778    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8779    let workspace = window
 8780        .read_with(cx, |mw, _| mw.workspace().clone())
 8781        .unwrap();
 8782    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8783    let panel = workspace.update_in(cx, ProjectPanel::new);
 8784    cx.run_until_parked();
 8785
 8786    panel.update(cx, |panel, cx| {
 8787        let project = panel.project.read(cx);
 8788        let worktree = project.visible_worktrees(cx).next().unwrap();
 8789        let worktree_id = worktree.read(cx).id();
 8790        let worktree = worktree.read(cx);
 8791
 8792        let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
 8793        let child_file = worktree
 8794            .entry_for_path(rel_path("parent_dir/child_file.txt"))
 8795            .unwrap();
 8796        let sibling_file = worktree
 8797            .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
 8798            .unwrap();
 8799        let child_dir = worktree
 8800            .entry_for_path(rel_path("parent_dir/child_dir"))
 8801            .unwrap();
 8802        let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
 8803        let other_file = worktree
 8804            .entry_for_path(rel_path("other_dir/other_file.txt"))
 8805            .unwrap();
 8806
 8807        // Test 1: Single item drag, don't highlight parent directory
 8808        let dragged_selection = DraggedSelection {
 8809            active_selection: SelectedEntry {
 8810                worktree_id,
 8811                entry_id: child_file.id,
 8812            },
 8813            marked_selections: Arc::new([SelectedEntry {
 8814                worktree_id,
 8815                entry_id: child_file.id,
 8816            }]),
 8817        };
 8818        let result =
 8819            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
 8820        assert_eq!(result, None, "Should not highlight parent of dragged item");
 8821
 8822        // Test 2: Single item drag, don't highlight sibling files
 8823        let result = panel.highlight_entry_for_selection_drag(
 8824            sibling_file,
 8825            worktree,
 8826            &dragged_selection,
 8827            cx,
 8828        );
 8829        assert_eq!(result, None, "Should not highlight sibling files");
 8830
 8831        // Test 3: Single item drag, highlight unrelated directory
 8832        let result =
 8833            panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
 8834        assert_eq!(
 8835            result,
 8836            Some(other_dir.id),
 8837            "Should highlight unrelated directory"
 8838        );
 8839
 8840        // Test 4: Single item drag, highlight sibling directory
 8841        let result =
 8842            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
 8843        assert_eq!(
 8844            result,
 8845            Some(child_dir.id),
 8846            "Should highlight sibling directory"
 8847        );
 8848
 8849        // Test 5: Multiple items drag, highlight parent directory
 8850        let dragged_selection = DraggedSelection {
 8851            active_selection: SelectedEntry {
 8852                worktree_id,
 8853                entry_id: child_file.id,
 8854            },
 8855            marked_selections: Arc::new([
 8856                SelectedEntry {
 8857                    worktree_id,
 8858                    entry_id: child_file.id,
 8859                },
 8860                SelectedEntry {
 8861                    worktree_id,
 8862                    entry_id: sibling_file.id,
 8863                },
 8864            ]),
 8865        };
 8866        let result =
 8867            panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
 8868        assert_eq!(
 8869            result,
 8870            Some(parent_dir.id),
 8871            "Should highlight parent with multiple items"
 8872        );
 8873
 8874        // Test 6: Target is file in different directory, highlight parent
 8875        let result =
 8876            panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
 8877        assert_eq!(
 8878            result,
 8879            Some(other_dir.id),
 8880            "Should highlight parent of target file"
 8881        );
 8882
 8883        // Test 7: Target is directory, always highlight
 8884        let result =
 8885            panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
 8886        assert_eq!(
 8887            result,
 8888            Some(child_dir.id),
 8889            "Should always highlight directories"
 8890        );
 8891    });
 8892}
 8893
 8894#[gpui::test]
 8895async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
 8896    init_test(cx);
 8897
 8898    let fs = FakeFs::new(cx.executor());
 8899    fs.insert_tree(
 8900        "/root1",
 8901        json!({
 8902            "src": {
 8903                "main.rs": "",
 8904                "lib.rs": ""
 8905            }
 8906        }),
 8907    )
 8908    .await;
 8909    fs.insert_tree(
 8910        "/root2",
 8911        json!({
 8912            "src": {
 8913                "main.rs": "",
 8914                "test.rs": ""
 8915            }
 8916        }),
 8917    )
 8918    .await;
 8919
 8920    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 8921    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 8922    let workspace = window
 8923        .read_with(cx, |mw, _| mw.workspace().clone())
 8924        .unwrap();
 8925    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 8926    let panel = workspace.update_in(cx, ProjectPanel::new);
 8927    cx.run_until_parked();
 8928
 8929    panel.update(cx, |panel, cx| {
 8930        let project = panel.project.read(cx);
 8931        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 8932
 8933        let worktree_a = &worktrees[0];
 8934        let main_rs_from_a = worktree_a
 8935            .read(cx)
 8936            .entry_for_path(rel_path("src/main.rs"))
 8937            .unwrap();
 8938
 8939        let worktree_b = &worktrees[1];
 8940        let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
 8941        let main_rs_from_b = worktree_b
 8942            .read(cx)
 8943            .entry_for_path(rel_path("src/main.rs"))
 8944            .unwrap();
 8945
 8946        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
 8947        let dragged_selection = DraggedSelection {
 8948            active_selection: SelectedEntry {
 8949                worktree_id: worktree_a.read(cx).id(),
 8950                entry_id: main_rs_from_a.id,
 8951            },
 8952            marked_selections: Arc::new([SelectedEntry {
 8953                worktree_id: worktree_a.read(cx).id(),
 8954                entry_id: main_rs_from_a.id,
 8955            }]),
 8956        };
 8957
 8958        let result = panel.highlight_entry_for_selection_drag(
 8959            src_dir_from_b,
 8960            worktree_b.read(cx),
 8961            &dragged_selection,
 8962            cx,
 8963        );
 8964        assert_eq!(
 8965            result,
 8966            Some(src_dir_from_b.id),
 8967            "Should highlight target directory from different worktree even with same relative path"
 8968        );
 8969
 8970        // Test dragging file from worktree A onto file with same relative path in worktree B
 8971        let result = panel.highlight_entry_for_selection_drag(
 8972            main_rs_from_b,
 8973            worktree_b.read(cx),
 8974            &dragged_selection,
 8975            cx,
 8976        );
 8977        assert_eq!(
 8978            result,
 8979            Some(src_dir_from_b.id),
 8980            "Should highlight parent of target file from different worktree"
 8981        );
 8982    });
 8983}
 8984
 8985#[gpui::test]
 8986async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
 8987    init_test(cx);
 8988
 8989    let fs = FakeFs::new(cx.executor());
 8990    fs.insert_tree(
 8991        "/root1",
 8992        json!({
 8993            "parent_dir": {
 8994                "child_file.txt": "",
 8995                "nested_dir": {
 8996                    "nested_file.txt": ""
 8997                }
 8998            },
 8999            "root_file.txt": ""
 9000        }),
 9001    )
 9002    .await;
 9003
 9004    fs.insert_tree(
 9005        "/root2",
 9006        json!({
 9007            "other_dir": {
 9008                "other_file.txt": ""
 9009            }
 9010        }),
 9011    )
 9012    .await;
 9013
 9014    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 9015    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9016    let workspace = window
 9017        .read_with(cx, |mw, _| mw.workspace().clone())
 9018        .unwrap();
 9019    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9020    let panel = workspace.update_in(cx, ProjectPanel::new);
 9021    cx.run_until_parked();
 9022
 9023    panel.update(cx, |panel, cx| {
 9024        let project = panel.project.read(cx);
 9025        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 9026        let worktree1 = worktrees[0].read(cx);
 9027        let worktree2 = worktrees[1].read(cx);
 9028        let worktree1_id = worktree1.id();
 9029        let _worktree2_id = worktree2.id();
 9030
 9031        let root1_entry = worktree1.root_entry().unwrap();
 9032        let root2_entry = worktree2.root_entry().unwrap();
 9033        let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
 9034        let child_file = worktree1
 9035            .entry_for_path(rel_path("parent_dir/child_file.txt"))
 9036            .unwrap();
 9037        let nested_file = worktree1
 9038            .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
 9039            .unwrap();
 9040        let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
 9041
 9042        // Test 1: Multiple entries - should always highlight background
 9043        let multiple_dragged_selection = DraggedSelection {
 9044            active_selection: SelectedEntry {
 9045                worktree_id: worktree1_id,
 9046                entry_id: child_file.id,
 9047            },
 9048            marked_selections: Arc::new([
 9049                SelectedEntry {
 9050                    worktree_id: worktree1_id,
 9051                    entry_id: child_file.id,
 9052                },
 9053                SelectedEntry {
 9054                    worktree_id: worktree1_id,
 9055                    entry_id: nested_file.id,
 9056                },
 9057            ]),
 9058        };
 9059
 9060        let result = panel.should_highlight_background_for_selection_drag(
 9061            &multiple_dragged_selection,
 9062            root1_entry.id,
 9063            cx,
 9064        );
 9065        assert!(result, "Should highlight background for multiple entries");
 9066
 9067        // Test 2: Single entry with non-empty parent path - should highlight background
 9068        let nested_dragged_selection = DraggedSelection {
 9069            active_selection: SelectedEntry {
 9070                worktree_id: worktree1_id,
 9071                entry_id: nested_file.id,
 9072            },
 9073            marked_selections: Arc::new([SelectedEntry {
 9074                worktree_id: worktree1_id,
 9075                entry_id: nested_file.id,
 9076            }]),
 9077        };
 9078
 9079        let result = panel.should_highlight_background_for_selection_drag(
 9080            &nested_dragged_selection,
 9081            root1_entry.id,
 9082            cx,
 9083        );
 9084        assert!(result, "Should highlight background for nested file");
 9085
 9086        // Test 3: Single entry at root level, same worktree - should NOT highlight background
 9087        let root_file_dragged_selection = DraggedSelection {
 9088            active_selection: SelectedEntry {
 9089                worktree_id: worktree1_id,
 9090                entry_id: root_file.id,
 9091            },
 9092            marked_selections: Arc::new([SelectedEntry {
 9093                worktree_id: worktree1_id,
 9094                entry_id: root_file.id,
 9095            }]),
 9096        };
 9097
 9098        let result = panel.should_highlight_background_for_selection_drag(
 9099            &root_file_dragged_selection,
 9100            root1_entry.id,
 9101            cx,
 9102        );
 9103        assert!(
 9104            !result,
 9105            "Should NOT highlight background for root file in same worktree"
 9106        );
 9107
 9108        // Test 4: Single entry at root level, different worktree - should highlight background
 9109        let result = panel.should_highlight_background_for_selection_drag(
 9110            &root_file_dragged_selection,
 9111            root2_entry.id,
 9112            cx,
 9113        );
 9114        assert!(
 9115            result,
 9116            "Should highlight background for root file from different worktree"
 9117        );
 9118
 9119        // Test 5: Single entry in subdirectory - should highlight background
 9120        let child_file_dragged_selection = DraggedSelection {
 9121            active_selection: SelectedEntry {
 9122                worktree_id: worktree1_id,
 9123                entry_id: child_file.id,
 9124            },
 9125            marked_selections: Arc::new([SelectedEntry {
 9126                worktree_id: worktree1_id,
 9127                entry_id: child_file.id,
 9128            }]),
 9129        };
 9130
 9131        let result = panel.should_highlight_background_for_selection_drag(
 9132            &child_file_dragged_selection,
 9133            root1_entry.id,
 9134            cx,
 9135        );
 9136        assert!(
 9137            result,
 9138            "Should highlight background for file with non-empty parent path"
 9139        );
 9140    });
 9141}
 9142
 9143#[gpui::test]
 9144async fn test_hide_root(cx: &mut gpui::TestAppContext) {
 9145    init_test(cx);
 9146
 9147    let fs = FakeFs::new(cx.executor());
 9148    fs.insert_tree(
 9149        "/root1",
 9150        json!({
 9151            "dir1": {
 9152                "file1.txt": "content",
 9153                "file2.txt": "content",
 9154            },
 9155            "dir2": {
 9156                "file3.txt": "content",
 9157            },
 9158            "file4.txt": "content",
 9159        }),
 9160    )
 9161    .await;
 9162
 9163    fs.insert_tree(
 9164        "/root2",
 9165        json!({
 9166            "dir3": {
 9167                "file5.txt": "content",
 9168            },
 9169            "file6.txt": "content",
 9170        }),
 9171    )
 9172    .await;
 9173
 9174    // Test 1: Single worktree with hide_root = false
 9175    {
 9176        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 9177        let window =
 9178            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9179        let workspace = window
 9180            .read_with(cx, |mw, _| mw.workspace().clone())
 9181            .unwrap();
 9182        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9183
 9184        cx.update(|_, cx| {
 9185            let settings = *ProjectPanelSettings::get_global(cx);
 9186            ProjectPanelSettings::override_global(
 9187                ProjectPanelSettings {
 9188                    hide_root: false,
 9189                    ..settings
 9190                },
 9191                cx,
 9192            );
 9193        });
 9194
 9195        let panel = workspace.update_in(cx, ProjectPanel::new);
 9196        cx.run_until_parked();
 9197
 9198        #[rustfmt::skip]
 9199        assert_eq!(
 9200            visible_entries_as_strings(&panel, 0..10, cx),
 9201            &[
 9202                "v root1",
 9203                "    > dir1",
 9204                "    > dir2",
 9205                "      file4.txt",
 9206            ],
 9207            "With hide_root=false and single worktree, root should be visible"
 9208        );
 9209    }
 9210
 9211    // Test 2: Single worktree with hide_root = true
 9212    {
 9213        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
 9214        let window =
 9215            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9216        let workspace = window
 9217            .read_with(cx, |mw, _| mw.workspace().clone())
 9218            .unwrap();
 9219        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9220
 9221        // Set hide_root to true
 9222        cx.update(|_, cx| {
 9223            let settings = *ProjectPanelSettings::get_global(cx);
 9224            ProjectPanelSettings::override_global(
 9225                ProjectPanelSettings {
 9226                    hide_root: true,
 9227                    ..settings
 9228                },
 9229                cx,
 9230            );
 9231        });
 9232
 9233        let panel = workspace.update_in(cx, ProjectPanel::new);
 9234        cx.run_until_parked();
 9235
 9236        assert_eq!(
 9237            visible_entries_as_strings(&panel, 0..10, cx),
 9238            &["> dir1", "> dir2", "  file4.txt",],
 9239            "With hide_root=true and single worktree, root should be hidden"
 9240        );
 9241
 9242        // Test expanding directories still works without root
 9243        toggle_expand_dir(&panel, "root1/dir1", cx);
 9244        assert_eq!(
 9245            visible_entries_as_strings(&panel, 0..10, cx),
 9246            &[
 9247                "v dir1  <== selected",
 9248                "      file1.txt",
 9249                "      file2.txt",
 9250                "> dir2",
 9251                "  file4.txt",
 9252            ],
 9253            "Should be able to expand directories even when root is hidden"
 9254        );
 9255    }
 9256
 9257    // Test 3: Multiple worktrees with hide_root = true
 9258    {
 9259        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 9260        let window =
 9261            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9262        let workspace = window
 9263            .read_with(cx, |mw, _| mw.workspace().clone())
 9264            .unwrap();
 9265        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9266
 9267        // Set hide_root to true
 9268        cx.update(|_, cx| {
 9269            let settings = *ProjectPanelSettings::get_global(cx);
 9270            ProjectPanelSettings::override_global(
 9271                ProjectPanelSettings {
 9272                    hide_root: true,
 9273                    ..settings
 9274                },
 9275                cx,
 9276            );
 9277        });
 9278
 9279        let panel = workspace.update_in(cx, ProjectPanel::new);
 9280        cx.run_until_parked();
 9281
 9282        assert_eq!(
 9283            visible_entries_as_strings(&panel, 0..10, cx),
 9284            &[
 9285                "v root1",
 9286                "    > dir1",
 9287                "    > dir2",
 9288                "      file4.txt",
 9289                "v root2",
 9290                "    > dir3",
 9291                "      file6.txt",
 9292            ],
 9293            "With hide_root=true and multiple worktrees, roots should still be visible"
 9294        );
 9295    }
 9296
 9297    // Test 4: Multiple worktrees with hide_root = false
 9298    {
 9299        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
 9300        let window =
 9301            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9302        let workspace = window
 9303            .read_with(cx, |mw, _| mw.workspace().clone())
 9304            .unwrap();
 9305        let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9306
 9307        cx.update(|_, cx| {
 9308            let settings = *ProjectPanelSettings::get_global(cx);
 9309            ProjectPanelSettings::override_global(
 9310                ProjectPanelSettings {
 9311                    hide_root: false,
 9312                    ..settings
 9313                },
 9314                cx,
 9315            );
 9316        });
 9317
 9318        let panel = workspace.update_in(cx, ProjectPanel::new);
 9319        cx.run_until_parked();
 9320
 9321        assert_eq!(
 9322            visible_entries_as_strings(&panel, 0..10, cx),
 9323            &[
 9324                "v root1",
 9325                "    > dir1",
 9326                "    > dir2",
 9327                "      file4.txt",
 9328                "v root2",
 9329                "    > dir3",
 9330                "      file6.txt",
 9331            ],
 9332            "With hide_root=false and multiple worktrees, roots should be visible"
 9333        );
 9334    }
 9335}
 9336
 9337#[gpui::test]
 9338async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
 9339    init_test_with_editor(cx);
 9340
 9341    let fs = FakeFs::new(cx.executor());
 9342    fs.insert_tree(
 9343        "/root",
 9344        json!({
 9345            "file1.txt": "content of file1",
 9346            "file2.txt": "content of file2",
 9347            "dir1": {
 9348                "file3.txt": "content of file3"
 9349            }
 9350        }),
 9351    )
 9352    .await;
 9353
 9354    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9355    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9356    let workspace = window
 9357        .read_with(cx, |mw, _| mw.workspace().clone())
 9358        .unwrap();
 9359    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9360    let panel = workspace.update_in(cx, ProjectPanel::new);
 9361    cx.run_until_parked();
 9362
 9363    let file1_path = "root/file1.txt";
 9364    let file2_path = "root/file2.txt";
 9365    select_path_with_mark(&panel, file1_path, cx);
 9366    select_path_with_mark(&panel, file2_path, cx);
 9367
 9368    panel.update_in(cx, |panel, window, cx| {
 9369        panel.compare_marked_files(&CompareMarkedFiles, window, cx);
 9370    });
 9371    cx.executor().run_until_parked();
 9372
 9373    workspace.update_in(cx, |workspace, _, cx| {
 9374        let active_items = workspace
 9375            .panes()
 9376            .iter()
 9377            .filter_map(|pane| pane.read(cx).active_item())
 9378            .collect::<Vec<_>>();
 9379        assert_eq!(active_items.len(), 1);
 9380        let diff_view = active_items
 9381            .into_iter()
 9382            .next()
 9383            .unwrap()
 9384            .downcast::<FileDiffView>()
 9385            .expect("Open item should be an FileDiffView");
 9386        assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
 9387        assert_eq!(
 9388            diff_view.tab_tooltip_text(cx).unwrap(),
 9389            format!(
 9390                "{}{}",
 9391                rel_path(file1_path).display(PathStyle::local()),
 9392                rel_path(file2_path).display(PathStyle::local())
 9393            )
 9394        );
 9395    });
 9396
 9397    let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
 9398    let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
 9399    let worktree_id = panel.update(cx, |panel, cx| {
 9400        panel
 9401            .project
 9402            .read(cx)
 9403            .worktrees(cx)
 9404            .next()
 9405            .unwrap()
 9406            .read(cx)
 9407            .id()
 9408    });
 9409
 9410    let expected_entries = [
 9411        SelectedEntry {
 9412            worktree_id,
 9413            entry_id: file1_entry_id,
 9414        },
 9415        SelectedEntry {
 9416            worktree_id,
 9417            entry_id: file2_entry_id,
 9418        },
 9419    ];
 9420    panel.update(cx, |panel, _cx| {
 9421        assert_eq!(
 9422            &panel.marked_entries, &expected_entries,
 9423            "Should keep marked entries after comparison"
 9424        );
 9425    });
 9426
 9427    panel.update(cx, |panel, cx| {
 9428        panel.project.update(cx, |_, cx| {
 9429            cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
 9430        })
 9431    });
 9432
 9433    panel.update(cx, |panel, _cx| {
 9434        assert_eq!(
 9435            &panel.marked_entries, &expected_entries,
 9436            "Marked entries should persist after focusing back on the project panel"
 9437        );
 9438    });
 9439}
 9440
 9441#[gpui::test]
 9442async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
 9443    init_test_with_editor(cx);
 9444
 9445    let fs = FakeFs::new(cx.executor());
 9446    fs.insert_tree(
 9447        "/root",
 9448        json!({
 9449            "file1.txt": "content of file1",
 9450            "file2.txt": "content of file2",
 9451            "dir1": {},
 9452            "dir2": {
 9453                "file3.txt": "content of file3"
 9454            }
 9455        }),
 9456    )
 9457    .await;
 9458
 9459    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9460    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9461    let workspace = window
 9462        .read_with(cx, |mw, _| mw.workspace().clone())
 9463        .unwrap();
 9464    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9465    let panel = workspace.update_in(cx, ProjectPanel::new);
 9466    cx.run_until_parked();
 9467
 9468    // Test 1: When only one file is selected, there should be no compare option
 9469    select_path(&panel, "root/file1.txt", cx);
 9470
 9471    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9472    assert_eq!(
 9473        selected_files, None,
 9474        "Should not have compare option when only one file is selected"
 9475    );
 9476
 9477    // Test 2: When multiple files are selected, there should be a compare option
 9478    select_path_with_mark(&panel, "root/file1.txt", cx);
 9479    select_path_with_mark(&panel, "root/file2.txt", cx);
 9480
 9481    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9482    assert!(
 9483        selected_files.is_some(),
 9484        "Should have files selected for comparison"
 9485    );
 9486    if let Some((file1, file2)) = selected_files {
 9487        assert!(
 9488            file1.to_string_lossy().ends_with("file1.txt")
 9489                && file2.to_string_lossy().ends_with("file2.txt"),
 9490            "Should have file1.txt and file2.txt as the selected files when multi-selecting"
 9491        );
 9492    }
 9493
 9494    // Test 3: Selecting a directory shouldn't count as a comparable file
 9495    select_path_with_mark(&panel, "root/dir1", cx);
 9496
 9497    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9498    assert!(
 9499        selected_files.is_some(),
 9500        "Directory selection should not affect comparable files"
 9501    );
 9502    if let Some((file1, file2)) = selected_files {
 9503        assert!(
 9504            file1.to_string_lossy().ends_with("file1.txt")
 9505                && file2.to_string_lossy().ends_with("file2.txt"),
 9506            "Selecting a directory should not affect the number of comparable files"
 9507        );
 9508    }
 9509
 9510    // Test 4: Selecting one more file
 9511    select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
 9512
 9513    let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
 9514    assert!(
 9515        selected_files.is_some(),
 9516        "Directory selection should not affect comparable files"
 9517    );
 9518    if let Some((file1, file2)) = selected_files {
 9519        assert!(
 9520            file1.to_string_lossy().ends_with("file2.txt")
 9521                && file2.to_string_lossy().ends_with("file3.txt"),
 9522            "Selecting a directory should not affect the number of comparable files"
 9523        );
 9524    }
 9525}
 9526
 9527#[gpui::test]
 9528async fn test_reveal_in_file_manager_path_falls_back_to_worktree_root(
 9529    cx: &mut gpui::TestAppContext,
 9530) {
 9531    init_test(cx);
 9532
 9533    let fs = FakeFs::new(cx.executor());
 9534    fs.insert_tree(
 9535        "/root",
 9536        json!({
 9537            "file.txt": "content",
 9538            "dir": {},
 9539        }),
 9540    )
 9541    .await;
 9542
 9543    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9544    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9545    let workspace = window
 9546        .read_with(cx, |mw, _| mw.workspace().clone())
 9547        .unwrap();
 9548    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9549    let panel = workspace.update_in(cx, ProjectPanel::new);
 9550    cx.run_until_parked();
 9551
 9552    select_path(&panel, "root/file.txt", cx);
 9553    let selected_reveal_path = panel
 9554        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
 9555        .expect("selected entry should produce a reveal path");
 9556    assert!(
 9557        selected_reveal_path.ends_with(Path::new("file.txt")),
 9558        "Expected selected file path, got {:?}",
 9559        selected_reveal_path
 9560    );
 9561
 9562    panel.update(cx, |panel, _| {
 9563        panel.selection = None;
 9564        panel.marked_entries.clear();
 9565    });
 9566    let fallback_reveal_path = panel
 9567        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
 9568        .expect("project root should be used when selection is empty");
 9569    assert!(
 9570        fallback_reveal_path.ends_with(Path::new("root")),
 9571        "Expected worktree root path, got {:?}",
 9572        fallback_reveal_path
 9573    );
 9574}
 9575
 9576#[gpui::test]
 9577async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
 9578    init_test(cx);
 9579
 9580    let fs = FakeFs::new(cx.executor());
 9581    fs.insert_tree(
 9582        "/root",
 9583        json!({
 9584            ".hidden-file.txt": "hidden file content",
 9585            "visible-file.txt": "visible file content",
 9586            ".hidden-parent-dir": {
 9587                "nested-dir": {
 9588                    "file.txt": "file content",
 9589                }
 9590            },
 9591            "visible-dir": {
 9592                "file-in-visible.txt": "file content",
 9593                "nested": {
 9594                    ".hidden-nested-dir": {
 9595                        ".double-hidden-dir": {
 9596                            "deep-file-1.txt": "deep content 1",
 9597                            "deep-file-2.txt": "deep content 2"
 9598                        },
 9599                        "hidden-nested-file-1.txt": "hidden nested 1",
 9600                        "hidden-nested-file-2.txt": "hidden nested 2"
 9601                    },
 9602                    "visible-nested-file.txt": "visible nested content"
 9603                }
 9604            }
 9605        }),
 9606    )
 9607    .await;
 9608
 9609    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9610    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9611    let workspace = window
 9612        .read_with(cx, |mw, _| mw.workspace().clone())
 9613        .unwrap();
 9614    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9615
 9616    cx.update(|_, cx| {
 9617        let settings = *ProjectPanelSettings::get_global(cx);
 9618        ProjectPanelSettings::override_global(
 9619            ProjectPanelSettings {
 9620                hide_hidden: false,
 9621                ..settings
 9622            },
 9623            cx,
 9624        );
 9625    });
 9626
 9627    let panel = workspace.update_in(cx, ProjectPanel::new);
 9628    cx.run_until_parked();
 9629
 9630    toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
 9631    toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
 9632    toggle_expand_dir(&panel, "root/visible-dir", cx);
 9633    toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
 9634    toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
 9635    toggle_expand_dir(
 9636        &panel,
 9637        "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
 9638        cx,
 9639    );
 9640
 9641    let expanded = [
 9642        "v root",
 9643        "    v .hidden-parent-dir",
 9644        "        v nested-dir",
 9645        "              file.txt",
 9646        "    v visible-dir",
 9647        "        v nested",
 9648        "            v .hidden-nested-dir",
 9649        "                v .double-hidden-dir  <== selected",
 9650        "                      deep-file-1.txt",
 9651        "                      deep-file-2.txt",
 9652        "                  hidden-nested-file-1.txt",
 9653        "                  hidden-nested-file-2.txt",
 9654        "              visible-nested-file.txt",
 9655        "          file-in-visible.txt",
 9656        "      .hidden-file.txt",
 9657        "      visible-file.txt",
 9658    ];
 9659
 9660    assert_eq!(
 9661        visible_entries_as_strings(&panel, 0..30, cx),
 9662        &expanded,
 9663        "With hide_hidden=false, contents of hidden nested directory should be visible"
 9664    );
 9665
 9666    cx.update(|_, cx| {
 9667        let settings = *ProjectPanelSettings::get_global(cx);
 9668        ProjectPanelSettings::override_global(
 9669            ProjectPanelSettings {
 9670                hide_hidden: true,
 9671                ..settings
 9672            },
 9673            cx,
 9674        );
 9675    });
 9676
 9677    panel.update_in(cx, |panel, window, cx| {
 9678        panel.update_visible_entries(None, false, false, window, cx);
 9679    });
 9680    cx.run_until_parked();
 9681
 9682    assert_eq!(
 9683        visible_entries_as_strings(&panel, 0..30, cx),
 9684        &[
 9685            "v root",
 9686            "    v visible-dir",
 9687            "        v nested",
 9688            "              visible-nested-file.txt",
 9689            "          file-in-visible.txt",
 9690            "      visible-file.txt",
 9691        ],
 9692        "With hide_hidden=false, contents of hidden nested directory should be visible"
 9693    );
 9694
 9695    panel.update_in(cx, |panel, window, cx| {
 9696        let settings = *ProjectPanelSettings::get_global(cx);
 9697        ProjectPanelSettings::override_global(
 9698            ProjectPanelSettings {
 9699                hide_hidden: false,
 9700                ..settings
 9701            },
 9702            cx,
 9703        );
 9704        panel.update_visible_entries(None, false, false, window, cx);
 9705    });
 9706    cx.run_until_parked();
 9707
 9708    assert_eq!(
 9709        visible_entries_as_strings(&panel, 0..30, cx),
 9710        &expanded,
 9711        "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
 9712    );
 9713}
 9714
 9715fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 9716    let path = rel_path(path);
 9717    panel.update_in(cx, |panel, window, cx| {
 9718        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9719            let worktree = worktree.read(cx);
 9720            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9721                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 9722                panel.update_visible_entries(
 9723                    Some((worktree.id(), entry_id)),
 9724                    false,
 9725                    false,
 9726                    window,
 9727                    cx,
 9728                );
 9729                return;
 9730            }
 9731        }
 9732        panic!("no worktree for path {:?}", path);
 9733    });
 9734    cx.run_until_parked();
 9735}
 9736
 9737fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
 9738    let path = rel_path(path);
 9739    panel.update(cx, |panel, cx| {
 9740        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9741            let worktree = worktree.read(cx);
 9742            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9743                let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
 9744                let entry = crate::SelectedEntry {
 9745                    worktree_id: worktree.id(),
 9746                    entry_id,
 9747                };
 9748                if !panel.marked_entries.contains(&entry) {
 9749                    panel.marked_entries.push(entry);
 9750                }
 9751                panel.selection = Some(entry);
 9752                return;
 9753            }
 9754        }
 9755        panic!("no worktree for path {:?}", path);
 9756    });
 9757}
 9758
 9759/// `leaf_path` is the full path to the leaf entry (e.g., "root/a/b/c")
 9760/// `active_ancestor_path` is the path to the folded component that should be active.
 9761fn select_folded_path_with_mark(
 9762    panel: &Entity<ProjectPanel>,
 9763    leaf_path: &str,
 9764    active_ancestor_path: &str,
 9765    cx: &mut VisualTestContext,
 9766) {
 9767    select_path_with_mark(panel, leaf_path, cx);
 9768    set_folded_active_ancestor(panel, leaf_path, active_ancestor_path, cx);
 9769}
 9770
 9771fn set_folded_active_ancestor(
 9772    panel: &Entity<ProjectPanel>,
 9773    leaf_path: &str,
 9774    active_ancestor_path: &str,
 9775    cx: &mut VisualTestContext,
 9776) {
 9777    let leaf_path = rel_path(leaf_path);
 9778    let active_ancestor_path = rel_path(active_ancestor_path);
 9779    panel.update(cx, |panel, cx| {
 9780        let mut leaf_entry_id = None;
 9781        let mut target_entry_id = None;
 9782
 9783        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9784            let worktree = worktree.read(cx);
 9785            if let Ok(relative_path) = leaf_path.strip_prefix(worktree.root_name()) {
 9786                leaf_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9787            }
 9788            if let Ok(relative_path) = active_ancestor_path.strip_prefix(worktree.root_name()) {
 9789                target_entry_id = worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9790            }
 9791        }
 9792
 9793        let leaf_entry_id =
 9794            leaf_entry_id.unwrap_or_else(|| panic!("no entry for leaf path {leaf_path:?}"));
 9795        let target_entry_id = target_entry_id
 9796            .unwrap_or_else(|| panic!("no entry for active path {active_ancestor_path:?}"));
 9797        let folded_ancestors = panel
 9798            .state
 9799            .ancestors
 9800            .get_mut(&leaf_entry_id)
 9801            .unwrap_or_else(|| panic!("leaf path {leaf_path:?} should be folded"));
 9802        let ancestor_ids = folded_ancestors.ancestors.clone();
 9803
 9804        let mut depth_for_target = None;
 9805        for depth in 0..ancestor_ids.len() {
 9806            let resolved_entry_id = if depth == 0 {
 9807                leaf_entry_id
 9808            } else {
 9809                ancestor_ids.get(depth).copied().unwrap_or(leaf_entry_id)
 9810            };
 9811            if resolved_entry_id == target_entry_id {
 9812                depth_for_target = Some(depth);
 9813                break;
 9814            }
 9815        }
 9816
 9817        folded_ancestors.current_ancestor_depth = depth_for_target.unwrap_or_else(|| {
 9818            panic!(
 9819                "active path {active_ancestor_path:?} is not part of folded ancestors {ancestor_ids:?}"
 9820            )
 9821        });
 9822    });
 9823}
 9824
 9825fn drag_selection_to(
 9826    panel: &Entity<ProjectPanel>,
 9827    target_path: &str,
 9828    is_file: bool,
 9829    cx: &mut VisualTestContext,
 9830) {
 9831    let target_entry = find_project_entry(panel, target_path, cx)
 9832        .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
 9833
 9834    panel.update_in(cx, |panel, window, cx| {
 9835        let selection = panel
 9836            .selection
 9837            .expect("a selection is required before dragging");
 9838        let drag = DraggedSelection {
 9839            active_selection: SelectedEntry {
 9840                worktree_id: selection.worktree_id,
 9841                entry_id: panel.resolve_entry(selection.entry_id),
 9842            },
 9843            marked_selections: Arc::from(panel.marked_entries.clone()),
 9844        };
 9845        panel.drag_onto(&drag, target_entry, is_file, window, cx);
 9846    });
 9847    cx.executor().run_until_parked();
 9848}
 9849
 9850fn find_project_entry(
 9851    panel: &Entity<ProjectPanel>,
 9852    path: &str,
 9853    cx: &mut VisualTestContext,
 9854) -> Option<ProjectEntryId> {
 9855    let path = rel_path(path);
 9856    panel.update(cx, |panel, cx| {
 9857        for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 9858            let worktree = worktree.read(cx);
 9859            if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
 9860                return worktree.entry_for_path(relative_path).map(|entry| entry.id);
 9861            }
 9862        }
 9863        panic!("no worktree for path {path:?}");
 9864    })
 9865}
 9866
 9867fn visible_entries_as_strings(
 9868    panel: &Entity<ProjectPanel>,
 9869    range: Range<usize>,
 9870    cx: &mut VisualTestContext,
 9871) -> Vec<String> {
 9872    let mut result = Vec::new();
 9873    let mut project_entries = HashSet::default();
 9874    let mut has_editor = false;
 9875
 9876    panel.update_in(cx, |panel, window, cx| {
 9877        panel.for_each_visible_entry(range, window, cx, &mut |project_entry, details, _, _| {
 9878            if details.is_editing {
 9879                assert!(!has_editor, "duplicate editor entry");
 9880                has_editor = true;
 9881            } else {
 9882                assert!(
 9883                    project_entries.insert(project_entry),
 9884                    "duplicate project entry {:?} {:?}",
 9885                    project_entry,
 9886                    details
 9887                );
 9888            }
 9889
 9890            let indent = "    ".repeat(details.depth);
 9891            let icon = if details.kind.is_dir() {
 9892                if details.is_expanded { "v " } else { "> " }
 9893            } else {
 9894                "  "
 9895            };
 9896            #[cfg(windows)]
 9897            let filename = details.filename.replace("\\", "/");
 9898            #[cfg(not(windows))]
 9899            let filename = details.filename;
 9900            let name = if details.is_editing {
 9901                format!("[EDITOR: '{}']", filename)
 9902            } else if details.is_processing {
 9903                format!("[PROCESSING: '{}']", filename)
 9904            } else {
 9905                filename
 9906            };
 9907            let selected = if details.is_selected {
 9908                "  <== selected"
 9909            } else {
 9910                ""
 9911            };
 9912            let marked = if details.is_marked {
 9913                "  <== marked"
 9914            } else {
 9915                ""
 9916            };
 9917
 9918            result.push(format!("{indent}{icon}{name}{selected}{marked}"));
 9919        });
 9920    });
 9921
 9922    result
 9923}
 9924
 9925/// Test that missing sort_mode field defaults to DirectoriesFirst
 9926#[gpui::test]
 9927async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
 9928    init_test(cx);
 9929
 9930    // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
 9931    let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
 9932    assert_eq!(
 9933        default_settings.sort_mode,
 9934        settings::ProjectPanelSortMode::DirectoriesFirst,
 9935        "sort_mode should default to DirectoriesFirst"
 9936    );
 9937}
 9938
 9939/// Test sort modes: DirectoriesFirst (default) vs Mixed
 9940#[gpui::test]
 9941async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
 9942    init_test(cx);
 9943
 9944    let fs = FakeFs::new(cx.executor());
 9945    fs.insert_tree(
 9946        "/root",
 9947        json!({
 9948            "zebra.txt": "",
 9949            "Apple": {},
 9950            "banana.rs": "",
 9951            "Carrot": {},
 9952            "aardvark.txt": "",
 9953        }),
 9954    )
 9955    .await;
 9956
 9957    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9958    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9959    let workspace = window
 9960        .read_with(cx, |mw, _| mw.workspace().clone())
 9961        .unwrap();
 9962    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 9963    let panel = workspace.update_in(cx, ProjectPanel::new);
 9964    cx.run_until_parked();
 9965
 9966    // Default sort mode should be DirectoriesFirst
 9967    assert_eq!(
 9968        visible_entries_as_strings(&panel, 0..50, cx),
 9969        &[
 9970            "v root",
 9971            "    > Apple",
 9972            "    > Carrot",
 9973            "      aardvark.txt",
 9974            "      banana.rs",
 9975            "      zebra.txt",
 9976        ]
 9977    );
 9978}
 9979
 9980#[gpui::test]
 9981async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
 9982    init_test(cx);
 9983
 9984    let fs = FakeFs::new(cx.executor());
 9985    fs.insert_tree(
 9986        "/root",
 9987        json!({
 9988            "Zebra.txt": "",
 9989            "apple": {},
 9990            "Banana.rs": "",
 9991            "carrot": {},
 9992            "Aardvark.txt": "",
 9993        }),
 9994    )
 9995    .await;
 9996
 9997    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
 9998    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 9999    let workspace = window
10000        .read_with(cx, |mw, _| mw.workspace().clone())
10001        .unwrap();
10002    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10003
10004    // Switch to Mixed mode
10005    cx.update(|_, cx| {
10006        cx.update_global::<SettingsStore, _>(|store, cx| {
10007            store.update_user_settings(cx, |settings| {
10008                settings.project_panel.get_or_insert_default().sort_mode =
10009                    Some(settings::ProjectPanelSortMode::Mixed);
10010            });
10011        });
10012    });
10013
10014    let panel = workspace.update_in(cx, ProjectPanel::new);
10015    cx.run_until_parked();
10016
10017    // Mixed mode: case-insensitive sorting
10018    // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
10019    assert_eq!(
10020        visible_entries_as_strings(&panel, 0..50, cx),
10021        &[
10022            "v root",
10023            "      Aardvark.txt",
10024            "    > apple",
10025            "      Banana.rs",
10026            "    > carrot",
10027            "      Zebra.txt",
10028        ]
10029    );
10030}
10031
10032#[gpui::test]
10033async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
10034    init_test(cx);
10035
10036    let fs = FakeFs::new(cx.executor());
10037    fs.insert_tree(
10038        "/root",
10039        json!({
10040            "Zebra.txt": "",
10041            "apple": {},
10042            "Banana.rs": "",
10043            "carrot": {},
10044            "Aardvark.txt": "",
10045        }),
10046    )
10047    .await;
10048
10049    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
10050    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10051    let workspace = window
10052        .read_with(cx, |mw, _| mw.workspace().clone())
10053        .unwrap();
10054    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10055
10056    // Switch to FilesFirst mode
10057    cx.update(|_, cx| {
10058        cx.update_global::<SettingsStore, _>(|store, cx| {
10059            store.update_user_settings(cx, |settings| {
10060                settings.project_panel.get_or_insert_default().sort_mode =
10061                    Some(settings::ProjectPanelSortMode::FilesFirst);
10062            });
10063        });
10064    });
10065
10066    let panel = workspace.update_in(cx, ProjectPanel::new);
10067    cx.run_until_parked();
10068
10069    // FilesFirst mode: files first, then directories (both case-insensitive)
10070    assert_eq!(
10071        visible_entries_as_strings(&panel, 0..50, cx),
10072        &[
10073            "v root",
10074            "      Aardvark.txt",
10075            "      Banana.rs",
10076            "      Zebra.txt",
10077            "    > apple",
10078            "    > carrot",
10079        ]
10080    );
10081}
10082
10083#[gpui::test]
10084async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
10085    init_test(cx);
10086
10087    let fs = FakeFs::new(cx.executor());
10088    fs.insert_tree(
10089        "/root",
10090        json!({
10091            "file2.txt": "",
10092            "dir1": {},
10093            "file1.txt": "",
10094        }),
10095    )
10096    .await;
10097
10098    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
10099    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10100    let workspace = window
10101        .read_with(cx, |mw, _| mw.workspace().clone())
10102        .unwrap();
10103    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10104    let panel = workspace.update_in(cx, ProjectPanel::new);
10105    cx.run_until_parked();
10106
10107    // Initially DirectoriesFirst
10108    assert_eq!(
10109        visible_entries_as_strings(&panel, 0..50, cx),
10110        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
10111    );
10112
10113    // Toggle to Mixed
10114    cx.update(|_, cx| {
10115        cx.update_global::<SettingsStore, _>(|store, cx| {
10116            store.update_user_settings(cx, |settings| {
10117                settings.project_panel.get_or_insert_default().sort_mode =
10118                    Some(settings::ProjectPanelSortMode::Mixed);
10119            });
10120        });
10121    });
10122    cx.run_until_parked();
10123
10124    assert_eq!(
10125        visible_entries_as_strings(&panel, 0..50, cx),
10126        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
10127    );
10128
10129    // Toggle back to DirectoriesFirst
10130    cx.update(|_, cx| {
10131        cx.update_global::<SettingsStore, _>(|store, cx| {
10132            store.update_user_settings(cx, |settings| {
10133                settings.project_panel.get_or_insert_default().sort_mode =
10134                    Some(settings::ProjectPanelSortMode::DirectoriesFirst);
10135            });
10136        });
10137    });
10138    cx.run_until_parked();
10139
10140    assert_eq!(
10141        visible_entries_as_strings(&panel, 0..50, cx),
10142        &["v root", "    > dir1", "      file1.txt", "      file2.txt",]
10143    );
10144}
10145
10146#[gpui::test]
10147async fn test_ensure_temporary_folding_when_creating_in_different_nested_dirs(
10148    cx: &mut gpui::TestAppContext,
10149) {
10150    init_test(cx);
10151
10152    // parent: accept
10153    run_create_file_in_folded_path_case(
10154        "parent",
10155        "root1/parent",
10156        "file_in_parent.txt",
10157        &[
10158            "v root1",
10159            "    v parent",
10160            "        > subdir/child",
10161            "          [EDITOR: '']  <== selected",
10162        ],
10163        &[
10164            "v root1",
10165            "    v parent",
10166            "        > subdir/child",
10167            "          file_in_parent.txt  <== selected  <== marked",
10168        ],
10169        true,
10170        cx,
10171    )
10172    .await;
10173
10174    // parent: cancel
10175    run_create_file_in_folded_path_case(
10176        "parent",
10177        "root1/parent",
10178        "file_in_parent.txt",
10179        &[
10180            "v root1",
10181            "    v parent",
10182            "        > subdir/child",
10183            "          [EDITOR: '']  <== selected",
10184        ],
10185        &["v root1", "    > parent/subdir/child  <== selected"],
10186        false,
10187        cx,
10188    )
10189    .await;
10190
10191    // subdir: accept
10192    run_create_file_in_folded_path_case(
10193        "subdir",
10194        "root1/parent/subdir",
10195        "file_in_subdir.txt",
10196        &[
10197            "v root1",
10198            "    v parent/subdir",
10199            "        > child",
10200            "          [EDITOR: '']  <== selected",
10201        ],
10202        &[
10203            "v root1",
10204            "    v parent/subdir",
10205            "        > child",
10206            "          file_in_subdir.txt  <== selected  <== marked",
10207        ],
10208        true,
10209        cx,
10210    )
10211    .await;
10212
10213    // subdir: cancel
10214    run_create_file_in_folded_path_case(
10215        "subdir",
10216        "root1/parent/subdir",
10217        "file_in_subdir.txt",
10218        &[
10219            "v root1",
10220            "    v parent/subdir",
10221            "        > child",
10222            "          [EDITOR: '']  <== selected",
10223        ],
10224        &["v root1", "    > parent/subdir/child  <== selected"],
10225        false,
10226        cx,
10227    )
10228    .await;
10229
10230    // child: accept
10231    run_create_file_in_folded_path_case(
10232        "child",
10233        "root1/parent/subdir/child",
10234        "file_in_child.txt",
10235        &[
10236            "v root1",
10237            "    v parent/subdir/child",
10238            "          [EDITOR: '']  <== selected",
10239        ],
10240        &[
10241            "v root1",
10242            "    v parent/subdir/child",
10243            "          file_in_child.txt  <== selected  <== marked",
10244        ],
10245        true,
10246        cx,
10247    )
10248    .await;
10249
10250    // child: cancel
10251    run_create_file_in_folded_path_case(
10252        "child",
10253        "root1/parent/subdir/child",
10254        "file_in_child.txt",
10255        &[
10256            "v root1",
10257            "    v parent/subdir/child",
10258            "          [EDITOR: '']  <== selected",
10259        ],
10260        &["v root1", "    v parent/subdir/child  <== selected"],
10261        false,
10262        cx,
10263    )
10264    .await;
10265}
10266
10267#[gpui::test]
10268async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu(
10269    cx: &mut gpui::TestAppContext,
10270) {
10271    init_test(cx);
10272
10273    let fs = FakeFs::new(cx.executor());
10274    fs.insert_tree(
10275        "/root1",
10276        json!({
10277            "parent": {
10278                "subdir": {
10279                    "child": {},
10280                }
10281            }
10282        }),
10283    )
10284    .await;
10285
10286    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
10287    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10288    let workspace = window
10289        .read_with(cx, |mw, _| mw.workspace().clone())
10290        .unwrap();
10291    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10292
10293    let panel = workspace.update_in(cx, |workspace, window, cx| {
10294        let panel = ProjectPanel::new(workspace, window, cx);
10295        workspace.add_panel(panel.clone(), window, cx);
10296        panel
10297    });
10298
10299    cx.update(|_, cx| {
10300        let settings = *ProjectPanelSettings::get_global(cx);
10301        ProjectPanelSettings::override_global(
10302            ProjectPanelSettings {
10303                auto_fold_dirs: true,
10304                ..settings
10305            },
10306            cx,
10307        );
10308    });
10309
10310    panel.update_in(cx, |panel, window, cx| {
10311        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
10312    });
10313    cx.run_until_parked();
10314
10315    select_folded_path_with_mark(
10316        &panel,
10317        "root1/parent/subdir/child",
10318        "root1/parent/subdir",
10319        cx,
10320    );
10321    panel.update(cx, |panel, _| {
10322        panel.marked_entries.clear();
10323    });
10324
10325    let parent_entry_id = find_project_entry(&panel, "root1/parent", cx)
10326        .expect("parent directory should exist for this test");
10327    let subdir_entry_id = find_project_entry(&panel, "root1/parent/subdir", cx)
10328        .expect("subdir directory should exist for this test");
10329    let child_entry_id = find_project_entry(&panel, "root1/parent/subdir/child", cx)
10330        .expect("child directory should exist for this test");
10331
10332    panel.update(cx, |panel, _| {
10333        let selection = panel
10334            .selection
10335            .expect("leaf directory should be selected before creating a new entry");
10336        assert_eq!(
10337            selection.entry_id, child_entry_id,
10338            "initial selection should be the folded leaf entry"
10339        );
10340        assert_eq!(
10341            panel.resolve_entry(selection.entry_id),
10342            subdir_entry_id,
10343            "active folded component should start at subdir"
10344        );
10345    });
10346
10347    panel.update_in(cx, |panel, window, cx| {
10348        panel.deploy_context_menu(
10349            gpui::point(gpui::px(1.), gpui::px(1.)),
10350            child_entry_id,
10351            window,
10352            cx,
10353        );
10354        panel.new_file(&NewFile, window, cx);
10355    });
10356    cx.run_until_parked();
10357    panel.update_in(cx, |panel, window, cx| {
10358        assert!(panel.filename_editor.read(cx).is_focused(window));
10359    });
10360    cx.run_until_parked();
10361
10362    set_folded_active_ancestor(&panel, "root1/parent/subdir", "root1/parent", cx);
10363
10364    panel.update_in(cx, |panel, window, cx| {
10365        panel.deploy_context_menu(
10366            gpui::point(gpui::px(2.), gpui::px(2.)),
10367            subdir_entry_id,
10368            window,
10369            cx,
10370        );
10371    });
10372    cx.run_until_parked();
10373
10374    panel.update(cx, |panel, _| {
10375        assert!(
10376            panel.state.edit_state.is_none(),
10377            "opening another context menu should blur the filename editor and discard edit state"
10378        );
10379        let selection = panel
10380            .selection
10381            .expect("selection should restore to the previously focused leaf entry");
10382        assert_eq!(
10383            selection.entry_id, child_entry_id,
10384            "blur-driven cancellation should restore the previous leaf selection"
10385        );
10386        assert_eq!(
10387            panel.resolve_entry(selection.entry_id),
10388            parent_entry_id,
10389            "temporary unfolded pending state should preserve the active ancestor chosen before blur"
10390        );
10391    });
10392
10393    panel.update_in(cx, |panel, window, cx| {
10394        panel.new_file(&NewFile, window, cx);
10395    });
10396    cx.run_until_parked();
10397    assert_eq!(
10398        visible_entries_as_strings(&panel, 0..10, cx),
10399        &[
10400            "v root1",
10401            "    v parent",
10402            "        > subdir/child",
10403            "          [EDITOR: '']  <== selected",
10404        ],
10405        "new file after blur should use the preserved active ancestor"
10406    );
10407    panel.update(cx, |panel, _| {
10408        let edit_state = panel
10409            .state
10410            .edit_state
10411            .as_ref()
10412            .expect("new file should enter edit state");
10413        assert_eq!(
10414            edit_state.temporarily_unfolded,
10415            Some(parent_entry_id),
10416            "temporary unfolding should now target parent after restoring the active ancestor"
10417        );
10418    });
10419
10420    let file_name = "created_after_blur.txt";
10421    panel
10422        .update_in(cx, |panel, window, cx| {
10423            panel.filename_editor.update(cx, |editor, cx| {
10424                editor.set_text(file_name, window, cx);
10425            });
10426            panel.confirm_edit(true, window, cx).expect(
10427                "confirm_edit should start creation for the file created after blur transition",
10428            )
10429        })
10430        .await
10431        .expect("creating file after blur transition should succeed");
10432    cx.run_until_parked();
10433
10434    assert!(
10435        fs.is_file(Path::new("/root1/parent/created_after_blur.txt"))
10436            .await,
10437        "file should be created under parent after active ancestor is restored to parent"
10438    );
10439    assert!(
10440        !fs.is_file(Path::new("/root1/parent/subdir/created_after_blur.txt"))
10441            .await,
10442        "file should not be created under subdir when parent is the active ancestor"
10443    );
10444}
10445
10446async fn run_create_file_in_folded_path_case(
10447    case_name: &str,
10448    active_ancestor_path: &str,
10449    created_file_name: &str,
10450    expected_temporary_state: &[&str],
10451    expected_final_state: &[&str],
10452    accept_creation: bool,
10453    cx: &mut gpui::TestAppContext,
10454) {
10455    let expected_collapsed_state = &["v root1", "    > parent/subdir/child  <== selected"];
10456
10457    let fs = FakeFs::new(cx.executor());
10458    fs.insert_tree(
10459        "/root1",
10460        json!({
10461            "parent": {
10462                "subdir": {
10463                    "child": {},
10464                }
10465            }
10466        }),
10467    )
10468    .await;
10469
10470    let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
10471    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
10472    let workspace = window
10473        .read_with(cx, |mw, _| mw.workspace().clone())
10474        .unwrap();
10475    let cx = &mut VisualTestContext::from_window(window.into(), cx);
10476
10477    let panel = workspace.update_in(cx, |workspace, window, cx| {
10478        let panel = ProjectPanel::new(workspace, window, cx);
10479        workspace.add_panel(panel.clone(), window, cx);
10480        panel
10481    });
10482
10483    cx.update(|_, cx| {
10484        let settings = *ProjectPanelSettings::get_global(cx);
10485        ProjectPanelSettings::override_global(
10486            ProjectPanelSettings {
10487                auto_fold_dirs: true,
10488                ..settings
10489            },
10490            cx,
10491        );
10492    });
10493
10494    panel.update_in(cx, |panel, window, cx| {
10495        panel.collapse_all_entries(&CollapseAllEntries, window, cx);
10496    });
10497    cx.run_until_parked();
10498
10499    select_folded_path_with_mark(
10500        &panel,
10501        "root1/parent/subdir/child",
10502        active_ancestor_path,
10503        cx,
10504    );
10505    panel.update(cx, |panel, _| {
10506        panel.marked_entries.clear();
10507    });
10508
10509    assert_eq!(
10510        visible_entries_as_strings(&panel, 0..10, cx),
10511        expected_collapsed_state,
10512        "case '{}' should start from a folded state",
10513        case_name
10514    );
10515
10516    panel.update_in(cx, |panel, window, cx| {
10517        panel.new_file(&NewFile, window, cx);
10518    });
10519    cx.run_until_parked();
10520    panel.update_in(cx, |panel, window, cx| {
10521        assert!(panel.filename_editor.read(cx).is_focused(window));
10522    });
10523    cx.run_until_parked();
10524    assert_eq!(
10525        visible_entries_as_strings(&panel, 0..10, cx),
10526        expected_temporary_state,
10527        "case '{}' ({}) should temporarily unfold the active ancestor while editing",
10528        case_name,
10529        if accept_creation { "accept" } else { "cancel" }
10530    );
10531
10532    let relative_directory = active_ancestor_path
10533        .strip_prefix("root1/")
10534        .expect("active_ancestor_path should start with root1/");
10535    let created_file_path = PathBuf::from("/root1")
10536        .join(relative_directory)
10537        .join(created_file_name);
10538
10539    if accept_creation {
10540        panel
10541            .update_in(cx, |panel, window, cx| {
10542                panel.filename_editor.update(cx, |editor, cx| {
10543                    editor.set_text(created_file_name, window, cx);
10544                });
10545                panel.confirm_edit(true, window, cx).unwrap()
10546            })
10547            .await
10548            .unwrap();
10549        cx.run_until_parked();
10550
10551        assert_eq!(
10552            visible_entries_as_strings(&panel, 0..10, cx),
10553            expected_final_state,
10554            "case '{}' should keep the newly created file selected and marked after accept",
10555            case_name
10556        );
10557        assert!(
10558            fs.is_file(created_file_path.as_path()).await,
10559            "case '{}' should create file '{}'",
10560            case_name,
10561            created_file_path.display()
10562        );
10563    } else {
10564        panel.update_in(cx, |panel, window, cx| {
10565            panel.cancel(&Cancel, window, cx);
10566        });
10567        cx.run_until_parked();
10568
10569        assert_eq!(
10570            visible_entries_as_strings(&panel, 0..10, cx),
10571            expected_final_state,
10572            "case '{}' should keep the expected panel state after cancel",
10573            case_name
10574        );
10575        assert!(
10576            !fs.is_file(created_file_path.as_path()).await,
10577            "case '{}' should not create a file after cancel",
10578            case_name
10579        );
10580    }
10581}
10582
10583pub(crate) fn init_test(cx: &mut TestAppContext) {
10584    cx.update(|cx| {
10585        let settings_store = SettingsStore::test(cx);
10586        cx.set_global(settings_store);
10587        theme_settings::init(theme::LoadThemes::JustBase, cx);
10588        crate::init(cx);
10589
10590        cx.update_global::<SettingsStore, _>(|store, cx| {
10591            store.update_user_settings(cx, |settings| {
10592                settings
10593                    .project_panel
10594                    .get_or_insert_default()
10595                    .auto_fold_dirs = Some(false);
10596                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
10597            });
10598        });
10599    });
10600}
10601
10602fn init_test_with_editor(cx: &mut TestAppContext) {
10603    cx.update(|cx| {
10604        let app_state = AppState::test(cx);
10605        theme_settings::init(theme::LoadThemes::JustBase, cx);
10606        editor::init(cx);
10607        crate::init(cx);
10608        workspace::init(app_state, cx);
10609
10610        cx.update_global::<SettingsStore, _>(|store, cx| {
10611            store.update_user_settings(cx, |settings| {
10612                settings
10613                    .project_panel
10614                    .get_or_insert_default()
10615                    .auto_fold_dirs = Some(false);
10616                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
10617            });
10618        });
10619    });
10620}
10621
10622fn set_auto_open_settings(
10623    cx: &mut TestAppContext,
10624    auto_open_settings: ProjectPanelAutoOpenSettings,
10625) {
10626    cx.update(|cx| {
10627        cx.update_global::<SettingsStore, _>(|store, cx| {
10628            store.update_user_settings(cx, |settings| {
10629                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
10630            });
10631        })
10632    });
10633}
10634
10635fn ensure_single_file_is_opened(
10636    workspace: &Entity<Workspace>,
10637    expected_path: &str,
10638    cx: &mut VisualTestContext,
10639) {
10640    workspace.update_in(cx, |workspace, _, cx| {
10641        let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
10642        assert_eq!(worktrees.len(), 1);
10643        let worktree_id = worktrees[0].read(cx).id();
10644
10645        let open_project_paths = workspace
10646            .panes()
10647            .iter()
10648            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
10649            .collect::<Vec<_>>();
10650        assert_eq!(
10651            open_project_paths,
10652            vec![ProjectPath {
10653                worktree_id,
10654                path: Arc::from(rel_path(expected_path))
10655            }],
10656            "Should have opened file, selected in project panel"
10657        );
10658    });
10659}
10660
10661fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
10662    assert!(
10663        !cx.has_pending_prompt(),
10664        "Should have no prompts before the deletion"
10665    );
10666    panel.update_in(cx, |panel, window, cx| {
10667        panel.delete(&Delete { skip_prompt: false }, window, cx)
10668    });
10669    assert!(
10670        cx.has_pending_prompt(),
10671        "Should have a prompt after the deletion"
10672    );
10673    cx.simulate_prompt_answer("Delete");
10674    assert!(
10675        !cx.has_pending_prompt(),
10676        "Should have no prompts after prompt was replied to"
10677    );
10678    cx.executor().run_until_parked();
10679}
10680
10681fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
10682    assert!(
10683        !cx.has_pending_prompt(),
10684        "Should have no prompts before the deletion"
10685    );
10686    panel.update_in(cx, |panel, window, cx| {
10687        panel.delete(&Delete { skip_prompt: true }, window, cx)
10688    });
10689    assert!(!cx.has_pending_prompt(), "Should have received no prompts");
10690    cx.executor().run_until_parked();
10691}
10692
10693fn ensure_no_open_items_and_panes(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) {
10694    assert!(
10695        !cx.has_pending_prompt(),
10696        "Should have no prompts after deletion operation closes the file"
10697    );
10698    workspace.update_in(cx, |workspace, _window, cx| {
10699        let open_project_paths = workspace
10700            .panes()
10701            .iter()
10702            .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
10703            .collect::<Vec<_>>();
10704        assert!(
10705            open_project_paths.is_empty(),
10706            "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
10707        );
10708    });
10709}
10710
10711struct TestProjectItemView {
10712    focus_handle: FocusHandle,
10713    path: ProjectPath,
10714}
10715
10716struct TestProjectItem {
10717    path: ProjectPath,
10718}
10719
10720impl project::ProjectItem for TestProjectItem {
10721    fn try_open(
10722        _project: &Entity<Project>,
10723        path: &ProjectPath,
10724        cx: &mut App,
10725    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10726        let path = path.clone();
10727        Some(cx.spawn(async move |cx| Ok(cx.new(|_| Self { path }))))
10728    }
10729
10730    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10731        None
10732    }
10733
10734    fn project_path(&self, _: &App) -> Option<ProjectPath> {
10735        Some(self.path.clone())
10736    }
10737
10738    fn is_dirty(&self) -> bool {
10739        false
10740    }
10741}
10742
10743impl ProjectItem for TestProjectItemView {
10744    type Item = TestProjectItem;
10745
10746    fn for_project_item(
10747        _: Entity<Project>,
10748        _: Option<&Pane>,
10749        project_item: Entity<Self::Item>,
10750        _: &mut Window,
10751        cx: &mut Context<Self>,
10752    ) -> Self
10753    where
10754        Self: Sized,
10755    {
10756        Self {
10757            path: project_item.update(cx, |project_item, _| project_item.path.clone()),
10758            focus_handle: cx.focus_handle(),
10759        }
10760    }
10761}
10762
10763impl Item for TestProjectItemView {
10764    type Event = ();
10765
10766    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10767        "Test".into()
10768    }
10769}
10770
10771impl EventEmitter<()> for TestProjectItemView {}
10772
10773impl Focusable for TestProjectItemView {
10774    fn focus_handle(&self, _: &App) -> FocusHandle {
10775        self.focus_handle.clone()
10776    }
10777}
10778
10779impl Render for TestProjectItemView {
10780    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
10781        Empty
10782    }
10783}