Unnamed repository; edit this file 'description' to name the repository.
feat: flatten single-child dirs in file explorer (#14173)
| -rw-r--r-- | book/src/editor.md | 1 | ||||
| -rw-r--r-- | helix-term/src/ui/mod.rs | 60 | ||||
| -rw-r--r-- | helix-term/src/ui/picker.rs | 8 | ||||
| -rw-r--r-- | helix-view/src/editor.rs | 31 |
4 files changed, 80 insertions, 20 deletions
diff --git a/book/src/editor.md b/book/src/editor.md index 87b73873..101ace15 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -240,6 +240,7 @@ Note that the ignore files consulted by the file explorer when `ignore` is set t |`git-ignore` | Enables reading `.gitignore` files | `false` |`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `false` |`git-exclude` | Enables reading `.git/info/exclude` files | `false` +|`flatten-dirs` | Enables flattening single child directories | `true` ### `[editor.auto-pairs]` Section diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 5a4db046..58b6fc00 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -354,12 +354,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std Ok(picker) } -fn directory_content(path: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> { +fn directory_content(root: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> { use ignore::WalkBuilder; let config = editor.config(); - let mut walk_builder = WalkBuilder::new(path); + let mut walk_builder = WalkBuilder::new(root); let mut content: Vec<(PathBuf, bool)> = walk_builder .hidden(config.file_explorer.hidden) @@ -377,27 +377,41 @@ fn directory_content(path: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool) .filter_map(|entry| { entry .map(|entry| { - ( - entry.path().to_path_buf(), - entry - .file_type() - .is_some_and(|file_type| file_type.is_dir()), - ) + let is_dir = entry + .file_type() + .is_some_and(|file_type| file_type.is_dir()); + let mut path = entry.path().to_path_buf(); + if is_dir && path != root && config.file_explorer.flatten_dirs { + while let Some(single_child_directory) = get_child_if_single_dir(&path) { + path = single_child_directory; + } + } + (path, is_dir) }) .ok() - .filter(|entry| entry.0 != path) + .filter(|entry| entry.0 != root) }) .collect(); content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2))); - if path.parent().is_some() { - content.insert(0, (path.join(".."), true)); + if root.parent().is_some() { + content.insert(0, (root.join(".."), true)); } Ok(content) } +fn get_child_if_single_dir(path: &Path) -> Option<PathBuf> { + let mut entries = path.read_dir().ok()?; + let entry = entries.next()?.ok()?; + if entries.next().is_none() && entry.file_type().is_ok_and(|file_type| file_type.is_dir()) { + Some(entry.path()) + } else { + None + } +} + pub mod completers { use super::Utf8PathBuf; use crate::ui::prompt::Completion; @@ -770,3 +784,27 @@ pub mod completers { completions } } + +#[cfg(test)] +mod tests { + use std::fs::{create_dir, File}; + + use super::*; + + #[test] + fn test_get_child_if_single_dir() { + let root = tempfile::tempdir().unwrap(); + + assert_eq!(get_child_if_single_dir(root.path()), None); + + let dir = root.path().join("dir1"); + create_dir(&dir).unwrap(); + + assert_eq!(get_child_if_single_dir(root.path()), Some(dir)); + + let file = root.path().join("file"); + File::create(file).unwrap(); + + assert_eq!(get_child_if_single_dir(root.path()), None); + } +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index db54afc1..c485fd88 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -613,8 +613,12 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> { let files = super::directory_content(&path, editor)?; let file_names: Vec<_> = files .iter() - .filter_map(|(path, is_dir)| { - let name = path.file_name()?.to_string_lossy(); + .filter_map(|(file_path, is_dir)| { + let name = file_path + .strip_prefix(&path) + .map(|p| Some(p.as_os_str())) + .unwrap_or_else(|_| file_path.file_name())? + .to_string_lossy(); if *is_dir { Some((format!("{}/", name), true)) } else { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5cd2ae2c..76682920 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -221,7 +221,7 @@ impl Default for FilePickerConfig { } } -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct FileExplorerConfig { /// IgnoreOptions @@ -229,22 +229,39 @@ pub struct FileExplorerConfig { /// Whether to hide hidden files in file explorer and global search results. Defaults to false. pub hidden: bool, /// Enables following symlinks. - /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true. + /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to false. pub follow_symlinks: bool, - /// Enables reading ignore files from parent directories. Defaults to true. + /// Enables reading ignore files from parent directories. Defaults to false. pub parents: bool, /// Enables reading `.ignore` files. - /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true. + /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to false. pub ignore: bool, /// Enables reading `.gitignore` files. - /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true. + /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to false. pub git_ignore: bool, /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. - /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true. + /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to false. pub git_global: bool, /// Enables reading `.git/info/exclude` files. - /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true. + /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to false. pub git_exclude: bool, + /// Whether to flatten single-child directories in file explorer. Defaults to true. + pub flatten_dirs: bool, +} + +impl Default for FileExplorerConfig { + fn default() -> Self { + Self { + hidden: false, + follow_symlinks: false, + parents: false, + ignore: false, + git_ignore: false, + git_global: false, + git_exclude: false, + flatten_dirs: true, + } + } } fn serialize_alphabet<S>(alphabet: &[char], serializer: S) -> Result<S::Ok, S::Error> |