project_panel: Add hidden files glob patterns and action toggle hidden files visibility (#41532)
This PR adds the ability to configure which files are considered
"hidden" in the project panel and toggle their visibility with a
keyboard shortcut. Previously, the editor hardcoded dotfiles as hidden -
now users can customize the pattern and quickly show/hide them.
### Release Notes
- Added `project_panel::ToggleHideHidden` action with keyboard shortcuts
to toggle visibility of hidden files
- Added configurable `hidden_files` setting to customize which files are
marked as hidden (defaults to `**/.*` for dotfiles)
### Motivation
This change allows users to:
1. Quickly toggle hidden file visibility with a keyboard shortcut
2. Customize which files are considered "hidden" beyond just dotfiles
3. Better organize their project panel by hiding build artifacts, logs,
or other generated files
### Usage
**Toggle hidden files:**
- **macOS:** `cmd-alt-.`
- **Linux:** `ctrl-alt-.`
- **Windows:** `ctrl-alt-.`
**Customize patterns in settings:**
```json
{
"hidden_files": ["**/.*", "**/*.tmp", "**/build/**"]
}
```
### Changes
**Core Implementation:**
- Added `hidden_files` setting (defaults to `**/.*` to match current
dotfile behavior)
- Replaced hardcoded `name.starts_with('.')` logic with configurable
pattern matching using `PathMatcher`
- Hidden status propagates through directory hierarchies (if a directory
is hidden, all children inherit that status)
**User-Facing:**
- Added `ToggleHideHidden` action in the project panel
- Added keyboard shortcuts for all platforms
- Added settings UI entry for configuring `hidden_files` patterns
**Testing:**
- Added comprehensive test coverage validating default behavior, custom
patterns, propagation, and settings changes
### Implementation Notes
- Uses `PathMatcher` for efficient glob matching
- Settings changes automatically trigger worktree re-indexing
- No breaking changes - defaults maintain current behavior (hiding
dotfiles)
---
**Disclaimer:** This was implemented with a fair amount of copy/paste
(particularly the gitignore handling), trial and error, and a healthy
dose of Claude.
### Screenshots
**Project Panel with hidden files visible:**
<img width="1368" height="935" alt="Screenshot 2025-10-30 at 3 15 53 AM"
src="https://github.com/user-attachments/assets/1cbe90ce-504c-4f9b-bca8-bef02ab961be"
/>
**Project Panel with hidden files hidden:**
<img width="1363" height="917" alt="Screenshot 2025-10-30 at 3 16 07 AM"
src="https://github.com/user-attachments/assets/9297f43e-98c7-4b19-be8f-3934589d6451"
/>
**Toggle action in command palette:**
<img width="565" height="161" alt="Screenshot 2025-10-30 at 3 17 26 AM"
src="https://github.com/user-attachments/assets/4dc9e7b6-9c29-4972-b886-88d8018905da"
/>
Release Notes:
- Added the ability to configure glob patterns for files treated as
hidden in the project panel using the `hidden_files` setting.
- Added an action `project panel: toggle hidden files` to quickly show
or hide hidden files in the project panel.
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
co-authored by
Smit Barmase
parent
fb6e41d51e
commit
1b93242351
@@ -1057,6 +1057,92 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hidden_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
let dir = TempTree::new(json!({
|
||||
".gitignore": "**/target\n",
|
||||
".hidden_file": "content",
|
||||
".hidden_dir": {
|
||||
"nested.rs": "code",
|
||||
},
|
||||
"src": {
|
||||
"visible.rs": "code",
|
||||
},
|
||||
"logs": {
|
||||
"app.log": "logs",
|
||||
"debug.log": "logs",
|
||||
},
|
||||
"visible.txt": "content",
|
||||
}));
|
||||
|
||||
let tree = Worktree::local(
|
||||
dir.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true, 0)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_hidden))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), true),
|
||||
(rel_path(".hidden_dir"), true),
|
||||
(rel_path(".hidden_dir/nested.rs"), true),
|
||||
(rel_path(".hidden_file"), true),
|
||||
(rel_path("logs"), false),
|
||||
(rel_path("logs/app.log"), false),
|
||||
(rel_path("logs/debug.log"), false),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/visible.rs"), false),
|
||||
(rel_path("visible.txt"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true, 0)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_hidden))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path(".hidden_dir"), false),
|
||||
(rel_path(".hidden_dir/nested.rs"), false),
|
||||
(rel_path(".hidden_file"), false),
|
||||
(rel_path("logs"), false),
|
||||
(rel_path("logs/app.log"), true),
|
||||
(rel_path("logs/debug.log"), true),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/visible.rs"), false),
|
||||
(rel_path("visible.txt"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
Reference in New Issue
Block a user