project_panel_tests.rs

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