project_panel: Reveal in file manager when no entry is selected (#50866)

loadingalias created

Closes #48284

## Summary
- Fix `project_panel::RevealInFileManager` when no project panel entry
is selected.
 - Preserve existing selected entry behavior.
- Add fallback to reveal the last visible worktree root when selection
is empty.
 - Add regression test cov.

## Root Cause
`RevealInFileManager` previously depended on `selected_sub_entry()`.
When selection is cleared (e.g. click project panel background), Command
Palette dispatch had no target and no-op'd.


## Verification
 - `cargo fmt --all -- --check`
 - `./script/check-keymaps`
 - `./script/clippy -p project_panel`
 - `cargo test -p project_panel -- --nocapture`

## Manual Testing
 - Reproduced issue steps from #48284.
- Confirmed Command Palette `Project panel: Reveal in file manager` now
opens project root when selection is empty.
  - Confirmed selected file reveal behavior remains unchanged.
  - Confirmed context menu reveal behavior remains unchanged.

Release Notes:
- Fixed `Project panel: Reveal in file manager` to work even when no
project panel entry is selected.

Change summary

crates/project_panel/src/project_panel.rs       | 17 +++++
crates/project_panel/src/project_panel_tests.rs | 49 +++++++++++++++++++
2 files changed, 64 insertions(+), 2 deletions(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -3403,8 +3403,7 @@ impl ProjectPanel {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
-            let path = worktree.read(cx).absolutize(&entry.path);
+        if let Some(path) = self.reveal_in_file_manager_path(cx) {
             self.project
                 .update(cx, |project, cx| project.reveal_path(&path, cx));
         }
@@ -3761,6 +3760,20 @@ impl ProjectPanel {
         }
         Some((worktree, entry))
     }
+
+    fn reveal_in_file_manager_path(&self, cx: &App) -> Option<PathBuf> {
+        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
+            return Some(worktree.read(cx).absolutize(&entry.path));
+        }
+
+        let root_entry_id = self.state.last_worktree_root_id?;
+        let project = self.project.read(cx);
+        let worktree = project.worktree_for_entry(root_entry_id, cx)?;
+        let worktree = worktree.read(cx);
+        let root_entry = worktree.entry_for_id(root_entry_id)?;
+        Some(worktree.absolutize(&root_entry.path))
+    }
+
     fn selected_entry_handle<'a>(
         &self,
         cx: &'a App,

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -8670,6 +8670,55 @@ async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
     }
 }
 
+#[gpui::test]
+async fn test_reveal_in_file_manager_path_falls_back_to_worktree_root(
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "file.txt": "content",
+            "dir": {},
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/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);
+    let panel = workspace.update_in(cx, ProjectPanel::new);
+    cx.run_until_parked();
+
+    select_path(&panel, "root/file.txt", cx);
+    let selected_reveal_path = panel
+        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
+        .expect("selected entry should produce a reveal path");
+    assert!(
+        selected_reveal_path.ends_with(Path::new("file.txt")),
+        "Expected selected file path, got {:?}",
+        selected_reveal_path
+    );
+
+    panel.update(cx, |panel, _| {
+        panel.selection = None;
+        panel.marked_entries.clear();
+    });
+    let fallback_reveal_path = panel
+        .update(cx, |panel, cx| panel.reveal_in_file_manager_path(cx))
+        .expect("project root should be used when selection is empty");
+    assert!(
+        fallback_reveal_path.ends_with(Path::new("root")),
+        "Expected worktree root path, got {:?}",
+        fallback_reveal_path
+    );
+}
+
 #[gpui::test]
 async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
     init_test(cx);