project_panel_tests.rs

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