Fix tree display issue #36: Implement proper tree connectors

- Replace all └── with correct ├── and └── connectors based on tree position
- Add vertical │ connectors for proper tree visualization
- Implement build_tree_info() function to determine correct connectors
- Add comprehensive tests for various tree structures
- Update existing dotfiles test to match new proper tree format

Before: All entries used └── regardless of position
After: Proper tree structure with ├── for intermediate entries, └── for last entries, and │ for vertical connections
This commit is contained in:
bgreenwell
2025-09-06 09:32:23 -04:00
parent 37eef8c1f5
commit 6b1da887f6
2 changed files with 201 additions and 9 deletions

View File

@@ -69,7 +69,10 @@ pub fn run(args: &ViewArgs, ls_colors: &LsColors) -> anyhow::Result<()> {
let sort_options = args.to_sort_options();
sort::sort_entries(&mut entries, &sort_options);
for entry in entries {
// Build tree structure information
let tree_info = build_tree_info(&entries);
for (index, entry) in entries.iter().enumerate() {
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
if args.dirs_only && !is_dir {
continue;
@@ -131,7 +134,8 @@ pub fn run(args: &ViewArgs, ls_colors: &LsColors) -> anyhow::Result<()> {
String::new()
};
let indent = " ".repeat(entry.depth().saturating_sub(1));
let default_tree_info = (String::new(), "└──".to_string());
let (prefix, connector) = tree_info.get(&index).unwrap_or(&default_tree_info);
let name = entry.file_name().to_string_lossy();
let icon_str = if args.icons {
let (icon, color) = icons::get_icon_for_path(entry.path(), is_dir);
@@ -210,12 +214,12 @@ pub fn run(args: &ViewArgs, ls_colors: &LsColors) -> anyhow::Result<()> {
if writeln!(
io::stdout(),
"{}{}{}└── {}{}{}",
"{}{}{}{} {}{}{}",
git_status_str,
permissions_str.dimmed(),
indent,
prefix,
connector,
icon_str,
//styled_name,
final_name,
size_str.dimmed()
)
@@ -230,3 +234,60 @@ pub fn run(args: &ViewArgs, ls_colors: &LsColors) -> anyhow::Result<()> {
Ok(())
}
/// Builds tree structure information for proper connector display
/// Returns a map from entry index to (prefix, connector) tuple
fn build_tree_info(
entries: &[ignore::DirEntry],
) -> std::collections::HashMap<usize, (String, String)> {
use std::collections::HashMap;
let mut tree_info = HashMap::new();
for (index, entry) in entries.iter().enumerate() {
let depth = entry.depth();
let mut prefix = String::new();
// Build prefix by walking up the tree and checking each ancestor
let current_path = entry.path();
// For each depth level from 1 to current depth - 1
for level in 1..depth {
// Find the ancestor directory at this level
let ancestor_path = {
let mut path = current_path;
for _ in level..depth {
if let Some(parent) = path.parent() {
path = parent;
}
}
path
};
// Check if this ancestor has more siblings coming after it
let has_more_siblings = entries.iter().enumerate().any(|(later_index, later_entry)| {
later_index > index && // Must come after current entry
later_entry.depth() == level && // Same depth as ancestor
later_entry.path().parent() == ancestor_path.parent() // Same parent as ancestor
});
if has_more_siblings {
prefix.push_str("");
} else {
prefix.push_str(" ");
}
}
// Determine connector for this entry (├── vs └──)
let is_last_sibling = !entries.iter().enumerate().any(|(later_index, later_entry)| {
later_index > index && // Must come after current entry
later_entry.depth() == depth && // Same depth
later_entry.path().parent() == entry.path().parent() // Same parent
});
let connector = if is_last_sibling { "└──" } else { "├──" };
tree_info.insert(index, (prefix, connector.to_string()));
}
tree_info
}

View File

@@ -357,10 +357,10 @@ fn test_dotfiles_first_sorting() -> Result<(), Box<dyn std::error::Error>> {
let stdout = String::from_utf8(output.stdout)?;
// Order should be: .dotfolder -> folder -> .hidden.txt -> regular.txt
// Use full line matching to avoid substring issues
let dotfolder_line_pos = stdout.find("── .dotfolder").expect(".dotfolder line not found");
let folder_line_pos = stdout.find("── folder").expect("folder line not found");
let hidden_line_pos = stdout.find("── .hidden.txt").expect(".hidden.txt line not found");
// With proper tree connectors: ├── for first 3 items, └── for last item
let dotfolder_line_pos = stdout.find("── .dotfolder").expect(".dotfolder line not found");
let folder_line_pos = stdout.find("── folder").expect("folder line not found");
let hidden_line_pos = stdout.find("── .hidden.txt").expect(".hidden.txt line not found");
let regular_line_pos = stdout.find("└── regular.txt").expect("regular.txt line not found");
assert!(dotfolder_line_pos < folder_line_pos);
@@ -369,3 +369,134 @@ fn test_dotfiles_first_sorting() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[test]
fn test_tree_structure_display() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
// Create the exact structure from issue #36:
// .
// └── t1
// ├── t2
// │ ├── hello.md
// │ └── t3
// └── tmp.txt
// Create t1 directory
fs::create_dir(temp_dir.path().join("t1"))?;
// Create t2 subdirectory
fs::create_dir(temp_dir.path().join("t1/t2"))?;
// Create files and subdirectories
fs::write(temp_dir.path().join("t1/t2/hello.md"), "# Hello")?;
fs::create_dir(temp_dir.path().join("t1/t2/t3"))?;
fs::write(temp_dir.path().join("t1/tmp.txt"), "temporary content")?;
let mut cmd = Command::cargo_bin("lstr")?;
cmd.arg(temp_dir.path());
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
// Expected tree structure with proper connectors:
// t1 should use └── (last/only item in root)
// t2 should use ├── (not last in t1)
// tmp.txt should use └── (last in t1)
// hello.md should use ├── (not last in t2)
// t3 should use └── (last in t2)
// Check that we have proper tree connectors, not all └──
assert!(stdout.contains("└── t1"), "t1 should use └── connector");
assert!(stdout.contains("├── t2"), "t2 should use ├── connector (not last in parent)");
assert!(stdout.contains("└── tmp.txt"), "tmp.txt should use └── connector (last in parent)");
assert!(
stdout.contains("├── hello.md"),
"hello.md should use ├── connector (not last in parent)"
);
assert!(stdout.contains("└── t3"), "t3 should use └── connector (last in parent)");
// Verify we have vertical connectors for proper tree visualization
assert!(stdout.contains(""), "Should contain vertical tree connectors");
Ok(())
}
#[test]
fn test_tree_structure_with_dirs_first() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
// Create a structure to test --dirs-first with tree connectors
fs::create_dir(temp_dir.path().join("dir1"))?;
fs::create_dir(temp_dir.path().join("dir2"))?;
fs::write(temp_dir.path().join("file1.txt"), "content1")?;
fs::write(temp_dir.path().join("file2.txt"), "content2")?;
// Add some nested content
fs::write(temp_dir.path().join("dir1/nested.txt"), "nested content")?;
let mut cmd = Command::cargo_bin("lstr")?;
cmd.arg("--dirs-first").arg(temp_dir.path());
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
// With --dirs-first, directories should come first
// dir1 should use ├── (not last directory)
// dir2 should use ├── (not last directory, files come after)
// file1.txt should use ├── (not last file)
// file2.txt should use └── (last file)
assert!(stdout.contains("├── dir1"), "dir1 should use ├── connector");
assert!(stdout.contains("├── dir2"), "dir2 should use ├── connector");
assert!(stdout.contains("├── file1.txt"), "file1.txt should use ├── connector");
assert!(stdout.contains("└── file2.txt"), "file2.txt should use └── connector (last)");
Ok(())
}
#[test]
fn test_single_file_tree() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
// Single file should use └──
fs::write(temp_dir.path().join("single.txt"), "content")?;
let mut cmd = Command::cargo_bin("lstr")?;
cmd.arg(temp_dir.path());
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
assert!(stdout.contains("└── single.txt"), "single file should use └── connector");
Ok(())
}
#[test]
fn test_deep_nested_tree() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
// Create a deeper nesting structure: a/b/c/d/file.txt
fs::create_dir(temp_dir.path().join("a"))?;
fs::create_dir(temp_dir.path().join("a/b"))?;
fs::create_dir(temp_dir.path().join("a/b/c"))?;
fs::create_dir(temp_dir.path().join("a/b/c/d"))?;
fs::write(temp_dir.path().join("a/b/c/d/deep.txt"), "deep content")?;
// Add sibling to 'a' to test vertical connectors
fs::create_dir(temp_dir.path().join("sibling"))?;
let mut cmd = Command::cargo_bin("lstr")?;
cmd.arg(temp_dir.path());
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
// Should have proper vertical connectors for deep nesting
assert!(stdout.contains("├── a"), "a should use ├── (has sibling)");
assert!(stdout.contains("└── sibling"), "sibling should use └── (last)");
assert!(stdout.contains(""), "Should contain vertical connectors for deep nesting");
Ok(())
}