diff --git a/assets/settings/default.json b/assets/settings/default.json index eca9fa5a68..3d2711c295 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -427,6 +427,8 @@ "project_panel": { // Whether to show the project panel button in the status bar "button": true, + // Whether to hide the gitignore entries in the project panel. + "hide_gitignore": false, // Default width of the project panel. "default_width": 240, // Where to dock the project panel. Can be 'left' or 'right'. diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 60d1858289..3bb8d9f975 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -36,7 +36,7 @@ use project_panel_settings::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{update_settings_file, Settings, SettingsStore}; use smallvec::SmallVec; use std::any::TypeId; use std::{ @@ -197,6 +197,7 @@ actions!( Open, OpenPermanent, ToggleFocus, + ToggleHideGitIgnore, NewSearchInDirectory, UnfoldDirectory, FoldDirectory, @@ -233,6 +234,13 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::(window, cx); }); + + workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| { + let fs = workspace.app_state().fs.clone(); + update_settings_file::(fs, cx, move |setting, _| { + setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false)); + }) + }); }) .detach(); } @@ -414,6 +422,9 @@ impl ProjectPanel { cx.observe_global::(move |this, cx| { let new_settings = *ProjectPanelSettings::get_global(cx); if project_panel_settings != new_settings { + if project_panel_settings.hide_gitignore != new_settings.hide_gitignore { + this.update_visible_entries(None, cx); + } project_panel_settings = new_settings; this.update_diagnostics(cx); cx.notify(); @@ -1536,7 +1547,6 @@ impl ProjectPanel { if sanitized_entries.is_empty() { return None; } - let project = self.project.read(cx); let (worktree_id, worktree) = sanitized_entries .iter() @@ -1568,13 +1578,14 @@ impl ProjectPanel { // Remove all siblings that are being deleted except the last marked entry let snapshot = worktree.snapshot(); + let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore; let mut siblings: Vec<_> = ChildEntriesGitIter::new(&snapshot, parent_path) .filter(|sibling| { - sibling.id == latest_entry.id - || !marked_entries_in_worktree.contains(&&SelectedEntry { + (sibling.id == latest_entry.id) + || (!marked_entries_in_worktree.contains(&&SelectedEntry { worktree_id, entry_id: sibling.id, - }) + }) && (!hide_gitignore || !sibling.is_ignored)) }) .map(|entry| entry.to_owned()) .collect(); @@ -2590,7 +2601,9 @@ impl ProjectPanel { new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut Context, ) { - let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs; + let settings = ProjectPanelSettings::get_global(cx); + let auto_collapse_dirs = settings.auto_fold_dirs; + let hide_gitignore = settings.hide_gitignore; let project = self.project.read(cx); self.last_worktree_root_id = project .visible_worktrees(cx) @@ -2675,7 +2688,9 @@ impl ProjectPanel { } } auto_folded_ancestors.clear(); - visible_worktree_entries.push(entry.to_owned()); + if !hide_gitignore || !entry.is_ignored { + visible_worktree_entries.push(entry.to_owned()); + } let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id { entry.id == new_entry_id || { self.ancestors.get(&entry.id).map_or(false, |entries| { @@ -2688,7 +2703,7 @@ impl ProjectPanel { } else { false }; - if precedes_new_entry { + if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) { visible_worktree_entries.push(GitEntry { entry: Entry { id: NEW_ENTRY_ID, diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 61cb926069..b59c3611be 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -31,6 +31,7 @@ pub enum EntrySpacing { #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct ProjectPanelSettings { pub button: bool, + pub hide_gitignore: bool, pub default_width: Pixels, pub dock: ProjectPanelDockPosition, pub entry_spacing: EntrySpacing, @@ -93,6 +94,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub button: Option, + /// Whether to hide gitignore files in the project panel. + /// + /// Default: false + pub hide_gitignore: Option, /// Customize default width (in pixels) taken by project panel /// /// Default: 240 diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 4edb2e62e8..9869562bf2 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -3735,6 +3735,172 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + path!("/root"), + json!({ + "aa": "// Testing 1", + "bb": "// Testing 2", + "cc": "// Testing 3", + "dd": "// Testing 4", + "ee": "// Testing 5", + "ff": "// Testing 6", + "gg": "// Testing 7", + "hh": "// Testing 8", + "ii": "// Testing 8", + ".gitignore": "bb\ndd\nee\nff\nii\n'", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + // Test 1: Auto selection with one gitignored file next to the deleted file + cx.update(|_, cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_gitignore: true, + ..settings + }, + cx, + ); + }); + + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + select_path(&panel, "root/aa", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " .gitignore", + " aa <== selected", + " cc", + " gg", + " hh" + ], + "Initial state should hide files on .gitignore" + ); + + submit_deletion(&panel, cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " .gitignore", + " cc <== selected", + " gg", + " hh" + ], + "Should select next entry not on .gitignore" + ); + + // Test 2: Auto selection with many gitignored files next to the deleted file + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " .gitignore", + " gg <== selected", + " hh" + ], + "Should select next entry not on .gitignore" + ); + + // Test 3: Auto selection of entry before deleted file + select_path(&panel, "root/hh", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " .gitignore", + " gg", + " hh <== selected" + ], + "Should select next entry not on .gitignore" + ); + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " .gitignore", " gg <== selected"], + "Should select next entry not on .gitignore" + ); +} + +#[gpui::test] +async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + path!("/root"), + json!({ + "dir1": { + "file1": "// Testing", + "file2": "// Testing", + "file3": "// Testing" + }, + "aa": "// Testing", + ".gitignore": "file1\nfile3\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + cx.update(|_, cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_gitignore: true, + ..settings + }, + cx, + ); + }); + + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Test 1: Visible items should exclude files on gitignore + toggle_expand_dir(&panel, "root/dir1", cx); + select_path(&panel, "root/dir1/file2", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " v dir1", + " file2 <== selected", + " .gitignore", + " aa" + ], + "Initial state should hide files on .gitignore" + ); + submit_deletion(&panel, cx); + + // Test 2: Auto selection should go to the parent + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root", + " v dir1 <== selected", + " .gitignore", + " aa" + ], + "Initial state should hide files on .gitignore" + ); +} + #[gpui::test] async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx);