@@ -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() {
@@ -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) {