mirror of
https://github.com/bgreenwell/lstr.git
synced 2025-12-16 12:00:11 +01:00
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:
71
src/view.rs
71
src/view.rs
@@ -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
|
||||
}
|
||||
|
||||
139
tests/cli.rs
139
tests/cli.rs
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user