project_panel_tests.rs

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