diff --git a/helix-term/src/ui/filetree.rs b/helix-term/src/ui/filetree.rs index ceb8a9c6..d3e386e6 100644 --- a/helix-term/src/ui/filetree.rs +++ b/helix-term/src/ui/filetree.rs @@ -39,7 +39,7 @@ use gestures::{ DragState, MoveValidity, DOUBLE_CLICK_THRESHOLD, }; -use helix_view::editor::{Action, FiletreeConfig, FiletreePosition, FiletreeState}; +use helix_view::editor::{Action, FilePickerConfig, FiletreePosition, FiletreeState}; use helix_view::graphics::Rect; use helix_view::input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use helix_view::keyboard::{KeyCode, KeyModifiers}; @@ -348,7 +348,10 @@ impl Filetree { } /// Rebuild [`Self::entries`] from the current state and filesystem. - fn refresh_entries(&mut self, state: &FiletreeState, config: &FiletreeConfig) { + /// Takes the file picker config (not the filetree config) so the + /// panel respects the same ignore rules the picker does — see + /// [`read_directory`] for the contract. + fn refresh_entries(&mut self, state: &FiletreeState, config: &FilePickerConfig) { self.entries.clear(); if let Some(root) = state.root.as_deref() { list_directory_into(root, &state.expanded, config, 0, &mut self.entries); @@ -1222,7 +1225,7 @@ impl Component for Filetree { // Rebuild entries before clamping cursor/scroll, so clamps reflect // the actual list length. if self.dirty { - self.refresh_entries(&ctx.editor.filetree, &config.filetree); + self.refresh_entries(&ctx.editor.filetree, &config.file_picker); } // If a path was queued for reveal (e.g. active buffer on focus or an @@ -1713,7 +1716,7 @@ fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> { fn list_directory_into( dir: &Path, expanded: &HashSet, - config: &FiletreeConfig, + config: &FilePickerConfig, depth: usize, out: &mut Vec, ) { @@ -1738,19 +1741,64 @@ fn list_directory_into( } } -/// Read one directory level, honoring the panel's ignore-flag config. Returns -/// `(path, is_dir)` tuples sorted directories-first, then alphabetical. -fn read_directory(dir: &Path, config: &FiletreeConfig) -> Vec<(PathBuf, bool)> { +/// VCS-internal directories the panel always hides, regardless of the +/// file picker's `hidden` setting. These are tool-state directories +/// users navigate via tooling (`git`, `jj`, etc.), never via the file +/// tree — listing them only invites accidental edits to repository +/// internals. +/// +/// Separate from the watcher's `NOISY_DIR_NAMES` by intent: that list +/// is "these write so much we'd render-storm" and includes things like +/// `target/` and `node_modules/` that are legitimate to peek into +/// occasionally. This list is "you should never click on these from a +/// file panel." The two overlap on the VCS dirs and that's it. +const VCS_INTERNAL_DIRS: &[&str] = &[".git", ".jj", ".hg", ".svn"]; + +/// Whether `name` is one of the always-hidden VCS-internal directory +/// names. Pure: just a slice lookup. The case-sensitive comparison +/// matches what `Path::file_name` returns on the filesystems we +/// support. +fn is_vcs_internal(name: &str) -> bool { + VCS_INTERNAL_DIRS.iter().any(|d| *d == name) +} + +/// Read one directory level, honoring the file picker's ignore-flag +/// configuration so the panel filters entries the same way the picker +/// does. Returns `(path, is_dir)` tuples sorted directories-first, then +/// alphabetical. +/// +/// The panel deliberately doesn't carry its own copy of these knobs — +/// users configure ignore behavior once under `editor.file-picker.*` +/// and the picker, search, and the panel all see the same rules. The +/// custom-ignore filenames (`/ignore`, `.helix/ignore`) are +/// also picked up, mirroring `ui::file_picker`. +/// +/// One panel-specific override on top of the unified config: VCS- +/// internal directories (see [`VCS_INTERNAL_DIRS`]) are always hidden, +/// even when the user has `file-picker.hidden = false` (a common +/// setting for users who want dotfiles findable in the picker). +/// Without this, `.git/`, `.jj/`, etc. would clutter the tree on the +/// most useful picker configuration. +/// +/// The `max_depth(Some(1))` here is the panel's *own* walk strategy +/// (one level per expansion) and is independent of +/// `file_picker.max_depth` (which is for the picker's recursive +/// search). Trying to interleave them would just make the panel show +/// fewer entries than the picker for no semantic reason. +fn read_directory(dir: &Path, config: &FilePickerConfig) -> Vec<(PathBuf, bool)> { use ignore::WalkBuilder; let mut builder = WalkBuilder::new(dir); builder - // The `ignore` crate's `hidden(true)` means "skip hidden entries", - // which is the inverse of our `show_hidden`. Flip the sense. - .hidden(!config.show_hidden) + .hidden(config.hidden) .parents(config.parents) - .git_ignore(config.git_ignore) + .ignore(config.ignore) .follow_links(config.follow_symlinks) + .git_ignore(config.git_ignore) + .git_global(config.git_global) + .git_exclude(config.git_exclude) + .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) + .add_custom_ignore_filename(".helix/ignore") .max_depth(Some(1)); let mut content: Vec<(PathBuf, bool)> = builder @@ -1761,6 +1809,10 @@ fn read_directory(dir: &Path, config: &FiletreeConfig) -> Vec<(PathBuf, bool)> { if path == dir { return None; // skip the root entry itself } + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if is_vcs_internal(name) { + return None; + } let is_dir = path.is_dir(); Some((path, is_dir)) }) @@ -1984,6 +2036,43 @@ mod tests { assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists); } + /// VCS-internal directories are always filtered from the panel, even + /// when the user's file-picker config would surface them (e.g. + /// `file-picker.hidden = false` makes hidden dirs visible to the + /// picker; the panel should still hide tool meta-state). + #[test] + fn is_vcs_internal_table() { + // Closed input domain: enumerate the list explicitly so any + // future addition or rename to VCS_INTERNAL_DIRS surfaces here. + for name in [".git", ".jj", ".hg", ".svn"] { + assert!( + is_vcs_internal(name), + "{name} should be filtered as VCS-internal", + ); + } + // Non-matches: substring confusions, partial matches, + // non-dotfile prefixes. None of these are meta-state. + for name in [ + "src", + "Cargo.toml", + ".gitignore", + ".gitattributes", + "git", + ".gits", + "git_extras", + ".github", + ".direnv", + "target", + "node_modules", + "", + ] { + assert!( + !is_vcs_internal(name), + "{name} should NOT be filtered as VCS-internal", + ); + } + } + /// flat-list shape is correct under a few different expansion patterns. /// This is a closed-domain test: the inputs (`expanded` set choices over a /// known tree) are small and enumerable. @@ -2008,7 +2097,9 @@ mod tests { fs::write(root.join("b/sub/deep.txt"), "").unwrap(); fs::write(root.join("c.txt"), "").unwrap(); - let config = FiletreeConfig::default(); + // The picker config's defaults match the panel's defaults + // (hidden = true, etc.); see FilePickerConfig::default. + let config = FilePickerConfig::default(); // Nothing expanded: only root's children should appear, dirs first. let mut entries = Vec::new(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 101d3417..bd76ae3c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -258,17 +258,6 @@ pub struct FiletreeConfig { /// behavior of `e`'s reveal-on-focus, but applied automatically /// rather than on a keystroke. Defaults to true. pub follow_active_buffer: bool, - /// Whether to show hidden (dotfile) entries. Defaults to true. - pub show_hidden: bool, - /// Whether to honor `.gitignore`. Defaults to false. - pub git_ignore: bool, - /// Whether to recurse into parent directories when finding ignore files. - /// Defaults to true. - pub parents: bool, - /// Whether to follow symbolic links. Defaults to true. - pub follow_symlinks: bool, - /// Cap directory recursion depth. `None` means unbounded. Defaults to `None`. - pub max_depth: Option, /// Whether to display git status indicators on entries. Defaults to true. pub show_git_status: bool, /// Whether to register a recursive filesystem watcher rooted at the @@ -292,11 +281,6 @@ impl Default for FiletreeConfig { width: 30, min_remaining_editor_width: 80, follow_active_buffer: true, - show_hidden: true, - git_ignore: false, - parents: true, - follow_symlinks: true, - max_depth: None, show_git_status: true, watch_filesystem: true, nerd_fonts: false,