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