Fix open path prompt not showing hidden files (#46965)

Austin Cummings created

Closes #39036 

The open path prompt will now show hidden files when "." is entered.
Also fixes an issue with "open this directory" showing twice when used
by the "toolchain: add toolchain" prompt.

With a tree of 
```
zed-industries
├── .hidden
├── .hidden-file
├── zed
├── zed-working
├── zeta
└── zeta-dataset
```
**Before:**
<img width="656" height="174" alt="image"
src="https://github.com/user-attachments/assets/abf30ce3-b1c2-4a14-a45d-c17b6c3aef6f"
/>

**After (current directory view without inputting "."):**
<img width="648" height="261" alt="image"
src="https://github.com/user-attachments/assets/00c65546-32c1-4c85-a05c-53152ab2f942"
/>

**After (when inputting "." to see hidden entries):**
<img width="618" height="156" alt="image"
src="https://github.com/user-attachments/assets/8453ae89-b1a7-44d4-9f7d-ed89e55a7020"
/>


Release Notes:
- Made Zed's built in file picker to show all hidden files by default

Change summary

crates/open_path_prompt/src/open_path_prompt.rs       | 90 +++++++-----
crates/open_path_prompt/src/open_path_prompt_tests.rs | 35 ++++
2 files changed, 87 insertions(+), 38 deletions(-)

Detailed changes

crates/open_path_prompt/src/open_path_prompt.rs 🔗

@@ -225,7 +225,8 @@ impl OpenPathPrompt {
         cx: &mut Context<Workspace>,
     ) {
         workspace.toggle_modal(window, cx, |window, cx| {
-            let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
+            let delegate =
+                OpenPathDelegate::new(tx, lister.clone(), creating_path, cx).show_hidden();
             let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
             let mut query = lister.default_query(cx);
             if let Some(suggested_name) = suggested_name {
@@ -402,14 +403,15 @@ impl PickerDelegate for OpenPathDelegate {
                 return;
             };
 
+            if !hidden_entries {
+                new_entries.retain(|entry| !entry.path.string.starts_with('.'));
+            }
+
             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| !entry.path.string.starts_with('.'));
-            }
 
             if suffix.is_empty() {
                 let should_prepend_with_current_dir = this
@@ -489,6 +491,8 @@ impl PickerDelegate for OpenPathDelegate {
                     if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
                     {
                         None
+                    } else if !suffix.is_empty() && entry.path.string == current_dir {
+                        None
                     } else {
                         Some(&entry.path)
                     }
@@ -892,33 +896,6 @@ fn path_candidates(
         .collect()
 }
 
-#[cfg(target_os = "windows")]
-fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
-    let last_item = Path::new(&query)
-        .file_name()
-        .unwrap_or_default()
-        .to_string_lossy();
-    let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
-        (dir.to_string(), last_item.into_owned())
-    } else {
-        (query.to_string(), String::new())
-    };
-    match path_style {
-        PathStyle::Posix => {
-            if dir.is_empty() {
-                dir = "/".to_string();
-            }
-        }
-        PathStyle::Windows => {
-            if dir.len() < 3 {
-                dir = "C:\\".to_string();
-            }
-        }
-    }
-    (dir, suffix)
-}
-
-#[cfg(not(target_os = "windows"))]
 fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
     match path_style {
         PathStyle::Posix => {
@@ -933,17 +910,18 @@ fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String)
             (dir, suffix)
         }
         PathStyle::Windows => {
-            let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
-                (query[..index].to_string(), query[index + 1..].to_string())
+            let last_sep = query.rfind('\\').into_iter().chain(query.rfind('/')).max();
+            let (mut dir, suffix) = if let Some(index) = last_sep {
+                (
+                    query[..index + 1].to_string(),
+                    query[index + 1..].to_string(),
+                )
             } else {
                 (query, String::new())
             };
             if dir.len() < 3 {
                 dir = "C:\\".to_string();
             }
-            if !dir.ends_with('\\') {
-                dir.push('\\');
-            }
             (dir, suffix)
         }
     }
@@ -987,6 +965,34 @@ mod tests {
             get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
         assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
         assert_eq!(suffix, "");
+
+        let (dir, suffix) = get_dir_and_suffix("C:\\root\\.".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:\\root\\");
+        assert_eq!(suffix, ".");
+
+        let (dir, suffix) = get_dir_and_suffix("C:\\root\\..".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:\\root\\");
+        assert_eq!(suffix, "..");
+
+        let (dir, suffix) = get_dir_and_suffix("C:\\root\\.hidden".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:\\root\\");
+        assert_eq!(suffix, ".hidden");
+
+        let (dir, suffix) = get_dir_and_suffix("C:/root/".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:/root/");
+        assert_eq!(suffix, "");
+
+        let (dir, suffix) = get_dir_and_suffix("C:/root/Use".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:/root/");
+        assert_eq!(suffix, "Use");
+
+        let (dir, suffix) = get_dir_and_suffix("C:\\root/Use".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:\\root/");
+        assert_eq!(suffix, "Use");
+
+        let (dir, suffix) = get_dir_and_suffix("C:/root\\.hidden".into(), PathStyle::Windows);
+        assert_eq!(dir, "C:/root\\");
+        assert_eq!(suffix, ".hidden");
     }
 
     #[test]
@@ -1014,5 +1020,17 @@ mod tests {
         let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
         assert_eq!(dir, "/Users/Junkui/Documents/");
         assert_eq!(suffix, "");
+
+        let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
+        assert_eq!(dir, "/root/");
+        assert_eq!(suffix, ".");
+
+        let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
+        assert_eq!(dir, "/root/");
+        assert_eq!(suffix, "..");
+
+        let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
+        assert_eq!(dir, "/root/");
+        assert_eq!(suffix, ".hidden");
     }
 }

crates/open_path_prompt/src/open_path_prompt_tests.rs 🔗

@@ -19,6 +19,8 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
         .insert_tree(
             path!("/root"),
             json!({
+                ".a1": ".A1",
+                ".b1": ".B1",
                 "a1": "A1",
                 "a2": "A2",
                 "a3": "A3",
@@ -51,7 +53,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
     #[cfg(windows)]
     let expected_separator = ".\\";
 
-    // If the query ends with a slash, the picker should show the contents of the directory.
+    // If the query ends with a slash, the picker should show the contents of the directory and not show any of the hidden entries.
     let query = path!("/root/");
     insert_query(query, &picker, cx).await;
     assert_eq!(
@@ -94,6 +96,33 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
     let query = path!("/root/dir2/di");
     insert_query(query, &picker, cx).await;
     assert_eq!(collect_match_candidates(&picker, cx), vec!["dir3", "dir4"]);
+
+    // Don't show candidates for the query ".".
+    let query = path!("/root/.");
+    insert_query(query, &picker, cx).await;
+    assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
+
+    // Don't show any candidates for the query ".a".
+    let query = path!("/root/.a");
+    insert_query(query, &picker, cx).await;
+    assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
+
+    // Show candidates for the query "./".
+    // Should show current directory and contents.
+    let query = path!("/root/./");
+    insert_query(query, &picker, cx).await;
+    assert_eq!(
+        collect_match_candidates(&picker, cx),
+        vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"]
+    );
+
+    // Show candidates for the query "../". Show parent contents.
+    let query = path!("/root/dir1/../");
+    insert_query(query, &picker, cx).await;
+    assert_eq!(
+        collect_match_candidates(&picker, cx),
+        vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"]
+    );
 }
 
 #[gpui::test]
@@ -369,11 +398,13 @@ async fn test_open_path_prompt_with_show_hidden(cx: &mut TestAppContext) {
     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"]
     );
+
+    insert_query(path!("/root/."), &picker, cx).await;
+    assert_eq!(collect_match_candidates(&picker, cx), vec![".hidden"]);
 }
 
 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {