Fix missing directories in path prompt when show_hidden is enabled (#47983)

Joseph T. Lyons created

The `Select Toolchain Path` picker would skip directories that exist on
my system and display `open this directory` multiple times. `Personal`
is missing:

<img width="569" height="225" alt="SCR-20260129-tvcc"
src="https://github.com/user-attachments/assets/a78db373-fdfb-439d-a688-a4c81763c745"
/>

```
~/Projects> ls
╭───┬──────────┬──────┬────────┬─────────────╮
│ # │   name   │ type │  size  │  modified   │
├───┼──────────┼──────┼────────┼─────────────┤
│ 0 │ Personal │ dir  │ 1.5 kB │ 2 weeks ago │
│ 1 │ Work     │ dir  │ 1.0 kB │ 2 hours ago │
╰───┴──────────┴──────┴────────┴─────────────╯
~/Projects> 
```

Release Notes:

- Fixed a bug where directories could be missing from the `Select
Toolchain Path` path picker

Change summary

crates/open_path_prompt/src/open_path_prompt.rs       | 11 +-
crates/open_path_prompt/src/open_path_prompt_tests.rs | 47 +++++++++++-
2 files changed, 49 insertions(+), 9 deletions(-)

Detailed changes

crates/open_path_prompt/src/open_path_prompt.rs 🔗

@@ -402,12 +402,13 @@ impl PickerDelegate for OpenPathDelegate {
                 return;
             };
 
-            let mut max_id = 0;
+            let max_id = new_entries
+                .iter()
+                .map(|entry| entry.path.id)
+                .max()
+                .unwrap_or(0);
             if !suffix.starts_with('.') && !hidden_entries {
-                new_entries.retain(|entry| {
-                    max_id = max_id.max(entry.path.id);
-                    !entry.path.string.starts_with('.')
-                });
+                new_entries.retain(|entry| !entry.path.string.starts_with('.'));
             }
 
             if suffix.is_empty() {

crates/open_path_prompt/src/open_path_prompt_tests.rs 🔗

@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, cx);
+    let (picker, cx) = build_open_path_prompt(project, false, false, cx);
 
     insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await;
     assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
@@ -119,7 +119,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, cx);
+    let (picker, cx) = build_open_path_prompt(project, false, false, cx);
 
     // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
     let query = path!("/root");
@@ -227,7 +227,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, false, cx);
+    let (picker, cx) = build_open_path_prompt(project, false, false, cx);
 
     // Support both forward and backward slashes.
     let query = "C:/root/";
@@ -322,7 +322,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
 
     let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 
-    let (picker, cx) = build_open_path_prompt(project, true, cx);
+    let (picker, cx) = build_open_path_prompt(project, true, false, cx);
 
     insert_query(path!("/root"), &picker, cx).await;
     assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
@@ -343,6 +343,39 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
     assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
 }
 
+#[gpui::test]
+async fn test_open_path_prompt_with_show_hidden(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/root"),
+            json!({
+                "directory_1": {},
+                "directory_2": {},
+                ".hidden": {}
+            }),
+        )
+        .await;
+
+    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+
+    let (picker, cx) = build_open_path_prompt(project, false, true, cx);
+
+    #[cfg(not(windows))]
+    let expected_separator = "./";
+    #[cfg(windows)]
+    let expected_separator = ".\\";
+
+    insert_query(path!("/root/"), &picker, cx).await;
+
+    assert_eq!(
+        collect_match_candidates(&picker, cx),
+        vec![expected_separator, ".hidden", "directory_1", "directory_2"]
+    );
+}
+
 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
     cx.update(|cx| {
         let state = AppState::test(cx);
@@ -356,6 +389,7 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
 fn build_open_path_prompt(
     project: Entity<Project>,
     creating_path: bool,
+    show_hidden: bool,
     cx: &mut TestAppContext,
 ) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
     let (tx, _) = futures::channel::oneshot::channel();
@@ -365,6 +399,11 @@ fn build_open_path_prompt(
     (
         workspace.update_in(cx, |_, window, cx| {
             let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
+            let delegate = if show_hidden {
+                delegate.show_hidden()
+            } else {
+                delegate
+            };
             cx.new(|cx| {
                 let picker = Picker::uniform_list(delegate, window, cx)
                     .width(rems(34.))