Reveal always_included entries in Project Panel (#26197)

Ryan Hawkins created

If the user has the `auto_reveal` option enabled, as well as
`file_scan_inclusions` and opens a file that is gitignored but is also
set to be always included, that file won't be revealed in the project
panel. I've personally found this annoying, as the project panel can
provide useful context on where you are in a codebase. It also just
feels weird for it to be out of sync with the editor state.

Release Notes:

- Fixed the interaction between `auto_reveal`, `file_scan_inclusions`,
and `.gitignore` within the Project Panel. Files that are always
included will now be auto-revealed in the Project Panel, even if those
files are also gitignored.

Change summary

crates/project_panel/src/project_panel.rs | 119 ++++++++++++++++++++++++
1 file changed, 118 insertions(+), 1 deletion(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -4246,7 +4246,7 @@ impl ProjectPanel {
             if skip_ignored
                 && worktree
                     .entry_for_id(entry_id)
-                    .map_or(true, |entry| entry.is_ignored)
+                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
             {
                 return;
             }
@@ -7871,6 +7871,123 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
+                    worktree_settings.file_scan_exclusions = Some(Vec::new());
+                    worktree_settings.file_scan_inclusions =
+                        Some(vec!["always_included_but_ignored_dir/*".to_string()]);
+                });
+                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
+                    project_panel_settings.auto_reveal_entries = Some(false)
+                });
+            })
+        });
+
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            "/project_root",
+            json!({
+                ".git": {},
+                ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
+                "dir_1": {
+                    "file_1.py": "# File 1_1 contents",
+                    "file_2.py": "# File 1_2 contents",
+                    "file_3.py": "# File 1_3 contents",
+                    "gitignored_dir": {
+                        "file_a.py": "# File contents",
+                        "file_b.py": "# File contents",
+                        "file_c.py": "# File contents",
+                    },
+                },
+                "dir_2": {
+                    "file_1.py": "# File 2_1 contents",
+                    "file_2.py": "# File 2_2 contents",
+                    "file_3.py": "# File 2_3 contents",
+                },
+                "always_included_but_ignored_dir": {
+                    "file_a.py": "# File contents",
+                    "file_b.py": "# File contents",
+                    "file_c.py": "# File contents",
+                },
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/project_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);
+        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &[
+                "v project_root",
+                "    > .git",
+                "    > always_included_but_ignored_dir",
+                "    > dir_1",
+                "    > dir_2",
+                "      .gitignore",
+            ]
+        );
+
+        let gitignored_dir_file =
+            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
+        let always_included_but_ignored_dir_file = find_project_entry(
+            &panel,
+            "project_root/always_included_but_ignored_dir/file_a.py",
+            cx,
+        )
+        .expect("file that is .gitignored but set to always be included should have an entry");
+        assert_eq!(
+            gitignored_dir_file, None,
+            "File in the gitignored dir should not have an entry unless its directory is toggled"
+        );
+
+        toggle_expand_dir(&panel, "project_root/dir_1", cx);
+        cx.run_until_parked();
+        cx.update(|_, cx| {
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
+                    project_panel_settings.auto_reveal_entries = Some(true)
+                });
+            })
+        });
+
+        panel.update(cx, |panel, cx| {
+            panel.project.update(cx, |_, cx| {
+                cx.emit(project::Event::ActiveEntryChanged(Some(
+                    always_included_but_ignored_dir_file,
+                )))
+            })
+        });
+        cx.run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &[
+                "v project_root",
+                "    > .git",
+                "    v always_included_but_ignored_dir",
+                "          file_a.py  <== selected  <== marked",
+                "          file_b.py",
+                "          file_c.py",
+                "    v dir_1",
+                "        > gitignored_dir",
+                "          file_1.py",
+                "          file_2.py",
+                "          file_3.py",
+                "    > dir_2",
+                "      .gitignore",
+            ],
+            "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
+        );
+    }
+
     #[gpui::test]
     async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
         init_test_with_editor(cx);