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

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
This commit is contained in:
Austin Cummings 2026-01-31 00:57:31 -07:00 committed by GitHub
parent 03663b966d
commit 795eb34098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 38 deletions

View file

@ -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");
}
}

View file

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