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