From eee6b4c56ce1b048daeebe5b503d3ad023d21c17 Mon Sep 17 00:00:00 2001 From: Hamza Paracha <86984199+DevDonzo@users.noreply.github.com> Date: Wed, 22 Apr 2026 06:05:30 -0400 Subject: [PATCH] project_panel: Allow New File from an empty hidden-root project (#53947) This fixes #53869. Creating a new file from the project panel background menu failed when a single local project was empty and its root was hidden. In that state there are no visible entries to seed `expanded_dir_ids`, so the action returned early before opening the filename editor. This initializes that state lazily from the root entry when creating a new item, and adds a regression test for the empty hidden-root path. Release Notes: - Fixed creating a new file from the project panel context menu in empty local projects --- crates/project_panel/src/project_panel.rs | 17 ++-- .../project_panel/src/project_panel_tests.rs | 98 +++++++++++++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e9d3a16564b1bdd2f452cd2ac34f23b839f5cb9d..990040ac4bff79bfbc12dc44c07fdedc98ee3722 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2077,13 +2077,18 @@ impl ProjectPanel { let directory_id; let new_entry_id = self.resolve_entry(entry_id); - if let Some((worktree, expanded_dir_ids)) = self - .project - .read(cx) - .worktree_for_id(worktree_id, cx) - .zip(self.state.expanded_dir_ids.get_mut(&worktree_id)) - { + if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { let worktree = worktree.read(cx); + let expanded_dir_ids = match self.state.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(entry) => entry.into_mut(), + hash_map::Entry::Vacant(entry) => { + let Some(root_entry_id) = worktree.root_entry().map(|entry| entry.id) else { + return; + }; + entry.insert(vec![root_entry_id]) + } + }; + if let Some(mut entry) = worktree.entry_for_id(new_entry_id) { loop { if entry.is_dir() { diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 5cb9fe838142c454941dfa85650708e2e57886ce..4897b57937d6c66f95776f19922dddbda5a202a5 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -8065,6 +8065,104 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC ); } +#[gpui::test] +async fn test_context_menu_new_file_in_empty_hidden_root(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/root"), json!({})).await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + cx.update(|_, cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_root: true, + ..settings + }, + cx, + ); + }); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + cx.run_until_parked(); + + assert!( + visible_entries_as_strings(&panel, 0..20, cx).is_empty(), + "Empty worktree with hide_root=true should render no entries" + ); + + panel.update(cx, |panel, _| { + assert!( + panel.selection.is_none(), + "Project panel should start without a selection" + ); + assert!( + panel.state.last_worktree_root_id.is_some(), + "Project panel should still track the hidden root entry" + ); + }); + + panel.update_in(cx, |panel, window, cx| { + let root_entry_id = panel + .state + .last_worktree_root_id + .expect("hidden root should be available for background context menu actions"); + panel.deploy_context_menu( + gpui::point(gpui::px(1.), gpui::px(1.)), + root_entry_id, + window, + cx, + ); + panel.new_file(&NewFile, window, cx); + }); + cx.run_until_parked(); + + panel.update_in(cx, |panel, window, cx| { + assert!( + panel.filename_editor.read(cx).is_focused(window), + "New File from the background context menu should open the filename editor" + ); + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[" [EDITOR: ''] <== selected"], + "New file editor should appear at the hidden root level" + ); + + let confirm = panel.update_in(cx, |panel, window, cx| { + panel.filename_editor.update(cx, |editor, cx| { + editor.set_text("new_file_from_context_menu.txt", window, cx) + }); + panel.confirm_edit(true, window, cx).unwrap() + }); + confirm.await.unwrap(); + cx.run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[" new_file_from_context_menu.txt <== selected <== marked"], + "Confirmed file should appear at the hidden root level" + ); + + assert!( + fs.is_file(Path::new("/root/new_file_from_context_menu.txt")) + .await, + "File should be created in the empty root directory" + ); +} + #[cfg(windows)] #[gpui::test] async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) {