Coverage for src\llm_code_lens\utils\tree.py: 14%
80 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-05-25 12:07 +0300
« prev ^ index » next coverage.py v7.7.0, created at 2025-05-25 12:07 +0300
1"""
2Project tree structure generator.
3Creates ASCII tree visualization of project structure.
4"""
6from pathlib import Path
7from typing import List, Set, Dict
8import os
10class ProjectTree:
11 """Generates ASCII tree structure for projects."""
13 def __init__(self, ignore_patterns: List[str] = None, max_depth: int = 5):
14 self.ignore_patterns = ignore_patterns or []
15 self.max_depth = max_depth
16 self.tree_chars = {
17 'pipe': '│ ',
18 'tee': '├── ',
19 'last': '└── ',
20 'blank': ' '
21 }
23 def generate_tree(self, root_path: Path, excluded_paths: Set[str] = None) -> str:
24 """Generate ASCII tree structure."""
25 excluded_paths = excluded_paths or set()
27 tree_lines = [f"{root_path.name}/"]
28 self._build_tree_recursive(
29 root_path,
30 tree_lines,
31 "",
32 excluded_paths,
33 depth=0
34 )
36 return "\n".join(tree_lines)
38 def _build_tree_recursive(self, path: Path, tree_lines: List[str], prefix: str,
39 excluded_paths: Set[str], depth: int) -> None:
40 """Recursively build tree structure."""
42 if depth >= self.max_depth:
43 return
45 try:
46 # Get all items in directory
47 items = list(path.iterdir())
49 # Filter out ignored items
50 items = [item for item in items if not self._should_ignore(item, excluded_paths)]
52 # Sort directories first, then files
53 items.sort(key=lambda x: (not x.is_dir(), x.name.lower()))
55 # Process each item
56 for i, item in enumerate(items):
57 is_last = i == len(items) - 1
59 # Choose tree characters
60 current_prefix = self.tree_chars['last'] if is_last else self.tree_chars['tee']
61 next_prefix = prefix + (self.tree_chars['blank'] if is_last else self.tree_chars['pipe'])
63 # Add item to tree
64 item_name = item.name
65 if item.is_dir():
66 item_name += "/"
68 tree_lines.append(f"{prefix}{current_prefix}{item_name}")
70 # Recurse into directories
71 if item.is_dir() and depth < self.max_depth - 1:
72 self._build_tree_recursive(item, tree_lines, next_prefix, excluded_paths, depth + 1)
74 except PermissionError:
75 # Handle permission errors gracefully
76 tree_lines.append(f"{prefix}{self.tree_chars['last']}[Permission Denied]")
78 def _should_ignore(self, path: Path, excluded_paths: Set[str]) -> bool:
79 """Check if path should be ignored."""
80 path_str = str(path)
82 # Check explicit exclusions
83 if path_str in excluded_paths:
84 return True
86 # Check ignore patterns
87 from ..cli import should_ignore
88 return should_ignore(path, self.ignore_patterns)
90 def generate_summary_tree(self, root_path: Path, excluded_paths: Set[str] = None) -> str:
91 """Generate a summary tree showing only key directories and file counts."""
92 excluded_paths = excluded_paths or set()
94 structure = self._analyze_structure(root_path, excluded_paths)
96 summary_lines = [f"{root_path.name}/ ({structure['total_files']} files, {structure['total_dirs']} directories)"]
98 for dir_name, info in sorted(structure['directories'].items()):
99 file_count = info['file_count']
100 subdir_count = info['subdir_count']
101 summary_lines.append(f" {dir_name}/ ({file_count} files, {subdir_count} subdirs)")
103 # Show file type distribution
104 if structure['file_types']:
105 summary_lines.append(" File types:")
106 for ext, count in sorted(structure['file_types'].items()):
107 summary_lines.append(f" {ext}: {count} files")
109 return "\n".join(summary_lines)
111 def _analyze_structure(self, root_path: Path, excluded_paths: Set[str]) -> Dict:
112 """Analyze project structure for summary."""
113 structure = {
114 'total_files': 0,
115 'total_dirs': 0,
116 'directories': {},
117 'file_types': {}
118 }
120 for item in root_path.rglob('*'):
121 if self._should_ignore(item, excluded_paths):
122 continue
124 if item.is_file():
125 structure['total_files'] += 1
127 # Count file types
128 ext = item.suffix.lower() or 'no extension'
129 structure['file_types'][ext] = structure['file_types'].get(ext, 0) + 1
131 elif item.is_dir():
132 structure['total_dirs'] += 1
134 # Analyze immediate subdirectories of root
135 if item.parent == root_path:
136 dir_info = self._analyze_directory(item, excluded_paths)
137 structure['directories'][item.name] = dir_info
139 return structure
141 def _analyze_directory(self, dir_path: Path, excluded_paths: Set[str]) -> Dict:
142 """Analyze a single directory."""
143 file_count = 0
144 subdir_count = 0
146 try:
147 for item in dir_path.rglob('*'):
148 if self._should_ignore(item, excluded_paths):
149 continue
151 if item.is_file():
152 file_count += 1
153 elif item.is_dir() and item != dir_path:
154 subdir_count += 1
155 except PermissionError:
156 pass
158 return {
159 'file_count': file_count,
160 'subdir_count': subdir_count
161 }