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

1""" 

2Project tree structure generator. 

3Creates ASCII tree visualization of project structure. 

4""" 

5 

6from pathlib import Path 

7from typing import List, Set, Dict 

8import os 

9 

10class ProjectTree: 

11 """Generates ASCII tree structure for projects.""" 

12 

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 } 

22 

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() 

26 

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 ) 

35 

36 return "\n".join(tree_lines) 

37 

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.""" 

41 

42 if depth >= self.max_depth: 

43 return 

44 

45 try: 

46 # Get all items in directory 

47 items = list(path.iterdir()) 

48 

49 # Filter out ignored items 

50 items = [item for item in items if not self._should_ignore(item, excluded_paths)] 

51 

52 # Sort directories first, then files 

53 items.sort(key=lambda x: (not x.is_dir(), x.name.lower())) 

54 

55 # Process each item 

56 for i, item in enumerate(items): 

57 is_last = i == len(items) - 1 

58 

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']) 

62 

63 # Add item to tree 

64 item_name = item.name 

65 if item.is_dir(): 

66 item_name += "/" 

67 

68 tree_lines.append(f"{prefix}{current_prefix}{item_name}") 

69 

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) 

73 

74 except PermissionError: 

75 # Handle permission errors gracefully 

76 tree_lines.append(f"{prefix}{self.tree_chars['last']}[Permission Denied]") 

77 

78 def _should_ignore(self, path: Path, excluded_paths: Set[str]) -> bool: 

79 """Check if path should be ignored.""" 

80 path_str = str(path) 

81 

82 # Check explicit exclusions 

83 if path_str in excluded_paths: 

84 return True 

85 

86 # Check ignore patterns 

87 from ..cli import should_ignore 

88 return should_ignore(path, self.ignore_patterns) 

89 

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() 

93 

94 structure = self._analyze_structure(root_path, excluded_paths) 

95 

96 summary_lines = [f"{root_path.name}/ ({structure['total_files']} files, {structure['total_dirs']} directories)"] 

97 

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)") 

102 

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") 

108 

109 return "\n".join(summary_lines) 

110 

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 } 

119 

120 for item in root_path.rglob('*'): 

121 if self._should_ignore(item, excluded_paths): 

122 continue 

123 

124 if item.is_file(): 

125 structure['total_files'] += 1 

126 

127 # Count file types 

128 ext = item.suffix.lower() or 'no extension' 

129 structure['file_types'][ext] = structure['file_types'].get(ext, 0) + 1 

130 

131 elif item.is_dir(): 

132 structure['total_dirs'] += 1 

133 

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 

138 

139 return structure 

140 

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 

145 

146 try: 

147 for item in dir_path.rglob('*'): 

148 if self._should_ignore(item, excluded_paths): 

149 continue 

150 

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 

157 

158 return { 

159 'file_count': file_count, 

160 'subdir_count': subdir_count 

161 }