Unnamed repository; edit this file 'description' to name the repository.
feat: flatten single-child dirs in file explorer (#14173)
Sylvain Terrien 5 months ago
parent 8acfc55 · commit 55167c2
-rw-r--r--book/src/editor.md1
-rw-r--r--helix-term/src/ui/mod.rs60
-rw-r--r--helix-term/src/ui/picker.rs8
-rw-r--r--helix-view/src/editor.rs31
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>