Skip to content

Commit 38651c4

Browse files
sylvainterrienlittleblack111
authored andcommitted
feat: flatten single-child dirs in file explorer (helix-editor#14173)
1 parent 904916c commit 38651c4

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
@@ -377,12 +377,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
377377
Ok(picker)
378378
}
379379

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

383383
let config = editor.config();
384384

385-
let mut walk_builder = WalkBuilder::new(path);
385+
let mut walk_builder = WalkBuilder::new(root);
386386

387387
let mut content: Vec<(PathBuf, bool)> = walk_builder
388388
.hidden(config.file_explorer.hidden)
@@ -400,27 +400,41 @@ fn directory_content(path: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)
400400
.filter_map(|entry| {
401401
entry
402402
.map(|entry| {
403-
(
404-
entry.path().to_path_buf(),
405-
entry
406-
.file_type()
407-
.is_some_and(|file_type| file_type.is_dir()),
408-
)
403+
let is_dir = entry
404+
.file_type()
405+
.is_some_and(|file_type| file_type.is_dir());
406+
let mut path = entry.path().to_path_buf();
407+
if is_dir && path != root && config.file_explorer.flatten_dirs {
408+
while let Some(single_child_directory) = get_child_if_single_dir(&path) {
409+
path = single_child_directory;
410+
}
411+
}
412+
(path, is_dir)
409413
})
410414
.ok()
411-
.filter(|entry| entry.0 != path)
415+
.filter(|entry| entry.0 != root)
412416
})
413417
.collect();
414418

415419
content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2)));
416420

417-
if path.parent().is_some() {
418-
content.insert(0, (path.join(".."), true));
421+
if root.parent().is_some() {
422+
content.insert(0, (root.join(".."), true));
419423
}
420424

421425
Ok(content)
422426
}
423427

428+
fn get_child_if_single_dir(path: &Path) -> Option<PathBuf> {
429+
let mut entries = path.read_dir().ok()?;
430+
let entry = entries.next()?.ok()?;
431+
if entries.next().is_none() && entry.file_type().is_ok_and(|file_type| file_type.is_dir()) {
432+
Some(entry.path())
433+
} else {
434+
None
435+
}
436+
}
437+
424438
pub mod completers {
425439
use super::Utf8PathBuf;
426440
use crate::ui::prompt::Completion;
@@ -793,3 +807,27 @@ pub mod completers {
793807
completions
794808
}
795809
}
810+
811+
#[cfg(test)]
812+
mod tests {
813+
use std::fs::{create_dir, File};
814+
815+
use super::*;
816+
817+
#[test]
818+
fn test_get_child_if_single_dir() {
819+
let root = tempfile::tempdir().unwrap();
820+
821+
assert_eq!(get_child_if_single_dir(root.path()), None);
822+
823+
let dir = root.path().join("dir1");
824+
create_dir(&dir).unwrap();
825+
826+
assert_eq!(get_child_if_single_dir(root.path()), Some(dir));
827+
828+
let file = root.path().join("file");
829+
File::create(file).unwrap();
830+
831+
assert_eq!(get_child_if_single_dir(root.path()), None);
832+
}
833+
}

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)