Skip to content

Commit 55167c2

Browse files
feat: flatten single-child dirs in file explorer (#14173)
1 parent 8acfc55 commit 55167c2

File tree

4 files changed

+80
-20
lines changed

4 files changed

+80
-20
lines changed

book/src/editor.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ Note that the ignore files consulted by the file explorer when `ignore` is set t
240240
|`git-ignore` | Enables reading `.gitignore` files | `false`
241241
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `false`
242242
|`git-exclude` | Enables reading `.git/info/exclude` files | `false`
243+
|`flatten-dirs` | Enables flattening single child directories | `true`
243244

244245

245246
### `[editor.auto-pairs]` Section

helix-term/src/ui/mod.rs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
354354
Ok(picker)
355355
}
356356

357-
fn directory_content(path: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
357+
fn directory_content(root: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
358358
use ignore::WalkBuilder;
359359

360360
let config = editor.config();
361361

362-
let mut walk_builder = WalkBuilder::new(path);
362+
let mut walk_builder = WalkBuilder::new(root);
363363

364364
let mut content: Vec<(PathBuf, bool)> = walk_builder
365365
.hidden(config.file_explorer.hidden)
@@ -377,27 +377,41 @@ fn directory_content(path: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)
377377
.filter_map(|entry| {
378378
entry
379379
.map(|entry| {
380-
(
381-
entry.path().to_path_buf(),
382-
entry
383-
.file_type()
384-
.is_some_and(|file_type| file_type.is_dir()),
385-
)
380+
let is_dir = entry
381+
.file_type()
382+
.is_some_and(|file_type| file_type.is_dir());
383+
let mut path = entry.path().to_path_buf();
384+
if is_dir && path != root && config.file_explorer.flatten_dirs {
385+
while let Some(single_child_directory) = get_child_if_single_dir(&path) {
386+
path = single_child_directory;
387+
}
388+
}
389+
(path, is_dir)
386390
})
387391
.ok()
388-
.filter(|entry| entry.0 != path)
392+
.filter(|entry| entry.0 != root)
389393
})
390394
.collect();
391395

392396
content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2)));
393397

394-
if path.parent().is_some() {
395-
content.insert(0, (path.join(".."), true));
398+
if root.parent().is_some() {
399+
content.insert(0, (root.join(".."), true));
396400
}
397401

398402
Ok(content)
399403
}
400404

405+
fn get_child_if_single_dir(path: &Path) -> Option<PathBuf> {
406+
let mut entries = path.read_dir().ok()?;
407+
let entry = entries.next()?.ok()?;
408+
if entries.next().is_none() && entry.file_type().is_ok_and(|file_type| file_type.is_dir()) {
409+
Some(entry.path())
410+
} else {
411+
None
412+
}
413+
}
414+
401415
pub mod completers {
402416
use super::Utf8PathBuf;
403417
use crate::ui::prompt::Completion;
@@ -770,3 +784,27 @@ pub mod completers {
770784
completions
771785
}
772786
}
787+
788+
#[cfg(test)]
789+
mod tests {
790+
use std::fs::{create_dir, File};
791+
792+
use super::*;
793+
794+
#[test]
795+
fn test_get_child_if_single_dir() {
796+
let root = tempfile::tempdir().unwrap();
797+
798+
assert_eq!(get_child_if_single_dir(root.path()), None);
799+
800+
let dir = root.path().join("dir1");
801+
create_dir(&dir).unwrap();
802+
803+
assert_eq!(get_child_if_single_dir(root.path()), Some(dir));
804+
805+
let file = root.path().join("file");
806+
File::create(file).unwrap();
807+
808+
assert_eq!(get_child_if_single_dir(root.path()), None);
809+
}
810+
}

helix-term/src/ui/picker.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,12 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
613613
let files = super::directory_content(&path, editor)?;
614614
let file_names: Vec<_> = files
615615
.iter()
616-
.filter_map(|(path, is_dir)| {
617-
let name = path.file_name()?.to_string_lossy();
616+
.filter_map(|(file_path, is_dir)| {
617+
let name = file_path
618+
.strip_prefix(&path)
619+
.map(|p| Some(p.as_os_str()))
620+
.unwrap_or_else(|_| file_path.file_name())?
621+
.to_string_lossy();
618622
if *is_dir {
619623
Some((format!("{}/", name), true))
620624
} else {

helix-view/src/editor.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,30 +221,47 @@ impl Default for FilePickerConfig {
221221
}
222222
}
223223

224-
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225225
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
226226
pub struct FileExplorerConfig {
227227
/// IgnoreOptions
228228
/// Enables ignoring hidden files.
229229
/// Whether to hide hidden files in file explorer and global search results. Defaults to false.
230230
pub hidden: bool,
231231
/// Enables following symlinks.
232-
/// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
232+
/// Whether to follow symbolic links in file picker and file or directory completions. Defaults to false.
233233
pub follow_symlinks: bool,
234-
/// Enables reading ignore files from parent directories. Defaults to true.
234+
/// Enables reading ignore files from parent directories. Defaults to false.
235235
pub parents: bool,
236236
/// Enables reading `.ignore` files.
237-
/// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
237+
/// Whether to hide files listed in .ignore in file picker and global search results. Defaults to false.
238238
pub ignore: bool,
239239
/// Enables reading `.gitignore` files.
240-
/// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
240+
/// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to false.
241241
pub git_ignore: bool,
242242
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
243-
/// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
243+
/// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to false.
244244
pub git_global: bool,
245245
/// Enables reading `.git/info/exclude` files.
246-
/// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
246+
/// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to false.
247247
pub git_exclude: bool,
248+
/// Whether to flatten single-child directories in file explorer. Defaults to true.
249+
pub flatten_dirs: bool,
250+
}
251+
252+
impl Default for FileExplorerConfig {
253+
fn default() -> Self {
254+
Self {
255+
hidden: false,
256+
follow_symlinks: false,
257+
parents: false,
258+
ignore: false,
259+
git_ignore: false,
260+
git_global: false,
261+
git_exclude: false,
262+
flatten_dirs: true,
263+
}
264+
}
248265
}
249266

250267
fn serialize_alphabet<S>(alphabet: &[char], serializer: S) -> Result<S::Ok, S::Error>

0 commit comments

Comments
 (0)