reveal-cli 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
reveal/tree_view.py ADDED
@@ -0,0 +1,105 @@
1
+ """Directory tree view for reveal."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+ from .base import get_analyzer
7
+
8
+
9
+ def show_directory_tree(path: str, depth: int = 3, show_hidden: bool = False) -> str:
10
+ """Show directory tree with file info.
11
+
12
+ Args:
13
+ path: Directory path
14
+ depth: Maximum depth to traverse
15
+ show_hidden: Whether to show hidden files/dirs
16
+
17
+ Returns:
18
+ Formatted tree string
19
+ """
20
+ path = Path(path)
21
+
22
+ if not path.is_dir():
23
+ return f"Error: {path} is not a directory"
24
+
25
+ lines = [f"šŸ“ {path.name or path}/\n"]
26
+ _walk_directory(path, lines, depth=depth, show_hidden=show_hidden)
27
+
28
+ # Add navigation hint
29
+ lines.append(f"\n→ reveal {path}/<file>")
30
+
31
+ return '\n'.join(lines)
32
+
33
+
34
+ def _walk_directory(path: Path, lines: List[str], prefix: str = '', depth: int = 3, show_hidden: bool = False):
35
+ """Recursively walk directory and build tree."""
36
+ if depth <= 0:
37
+ return
38
+
39
+ try:
40
+ entries = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name))
41
+ except PermissionError:
42
+ return
43
+
44
+ # Filter hidden files/dirs
45
+ if not show_hidden:
46
+ entries = [e for e in entries if not e.name.startswith('.')]
47
+
48
+ for i, entry in enumerate(entries):
49
+ is_last = (i == len(entries) - 1)
50
+
51
+ # Tree characters
52
+ if is_last:
53
+ connector = '└── '
54
+ extension = ' '
55
+ else:
56
+ connector = 'ā”œā”€ā”€ '
57
+ extension = '│ '
58
+
59
+ if entry.is_file():
60
+ # Show file with metadata
61
+ file_info = _get_file_info(entry)
62
+ lines.append(f"{prefix}{connector}{file_info}")
63
+
64
+ elif entry.is_dir():
65
+ # Show directory
66
+ lines.append(f"{prefix}{connector}{entry.name}/")
67
+ # Recurse into subdirectory
68
+ _walk_directory(entry, lines, prefix + extension, depth - 1, show_hidden)
69
+
70
+
71
+ def _get_file_info(path: Path) -> str:
72
+ """Get formatted file info for tree display.
73
+
74
+ Returns:
75
+ Formatted string like "app.py (247 lines, Python)"
76
+ """
77
+ try:
78
+ # Try to get analyzer for this file
79
+ analyzer_class = get_analyzer(str(path))
80
+
81
+ if analyzer_class:
82
+ # Use analyzer to get info
83
+ analyzer = analyzer_class(str(path))
84
+ meta = analyzer.get_metadata()
85
+ file_type = analyzer.type_name
86
+
87
+ return f"{path.name} ({meta['lines']} lines, {file_type})"
88
+ else:
89
+ # No analyzer - just show basic info
90
+ stat = os.stat(path)
91
+ size = _format_size(stat.st_size)
92
+ return f"{path.name} ({size})"
93
+
94
+ except Exception:
95
+ # If anything fails, just show filename
96
+ return path.name
97
+
98
+
99
+ def _format_size(size: int) -> str:
100
+ """Format file size in human-readable form."""
101
+ for unit in ['B', 'KB', 'MB', 'GB']:
102
+ if size < 1024.0:
103
+ return f"{size:.1f} {unit}"
104
+ size /= 1024.0
105
+ return f"{size:.1f} TB"
reveal/treesitter.py ADDED
@@ -0,0 +1,281 @@
1
+ """Tree-sitter based analyzer for multi-language support."""
2
+
3
+ import warnings
4
+ from typing import Dict, List, Any, Optional
5
+ from .base import FileAnalyzer
6
+ from tree_sitter_languages import get_parser
7
+
8
+
9
+ class TreeSitterAnalyzer(FileAnalyzer):
10
+ """Base class for tree-sitter based analyzers.
11
+
12
+ Provides automatic extraction for ANY tree-sitter language!
13
+
14
+ Subclass just needs to set:
15
+ language (str): tree-sitter language name (e.g., 'python', 'rust', 'go')
16
+
17
+ Everything else is automatic:
18
+ - Structure extraction (imports, functions, classes, structs)
19
+ - Element extraction (get specific function/class)
20
+ - Line number tracking
21
+
22
+ Usage:
23
+ @register('.go', name='Go', icon='šŸ”·')
24
+ class GoAnalyzer(TreeSitterAnalyzer):
25
+ language = 'go'
26
+ # Done! Full support in 3 lines.
27
+ """
28
+
29
+ language: str = None # Set in subclass
30
+
31
+ def __init__(self, path: str):
32
+ super().__init__(path)
33
+ self.tree = None
34
+
35
+ if self.language:
36
+ self._parse_tree()
37
+
38
+ def _parse_tree(self):
39
+ """Parse file with tree-sitter."""
40
+ try:
41
+ with warnings.catch_warnings():
42
+ warnings.filterwarnings('ignore', category=FutureWarning, module='tree_sitter')
43
+ parser = get_parser(self.language)
44
+ self.tree = parser.parse(self.content.encode('utf-8'))
45
+ except Exception as e:
46
+ # Parsing failed - fall back to text analysis
47
+ self.tree = None
48
+
49
+ def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
50
+ """Extract structure using tree-sitter.
51
+
52
+ Returns imports, functions, classes, structs, etc.
53
+ Works for ANY tree-sitter language!
54
+ """
55
+ if not self.tree:
56
+ return {}
57
+
58
+ structure = {}
59
+
60
+ # Extract common elements
61
+ structure['imports'] = self._extract_imports()
62
+ structure['functions'] = self._extract_functions()
63
+ structure['classes'] = self._extract_classes()
64
+ structure['structs'] = self._extract_structs()
65
+
66
+ # Remove empty categories
67
+ return {k: v for k, v in structure.items() if v}
68
+
69
+ def _extract_imports(self) -> List[Dict[str, Any]]:
70
+ """Extract import statements."""
71
+ imports = []
72
+
73
+ # Common import node types across languages
74
+ import_types = [
75
+ 'import_statement', # Python, JavaScript
76
+ 'import_declaration', # Go, Java
77
+ 'use_declaration', # Rust
78
+ 'using_directive', # C#
79
+ 'import_from_statement', # Python
80
+ ]
81
+
82
+ for import_type in import_types:
83
+ nodes = self._find_nodes_by_type(import_type)
84
+ for node in nodes:
85
+ imports.append({
86
+ 'line': node.start_point[0] + 1,
87
+ 'content': self._get_node_text(node),
88
+ })
89
+
90
+ return imports
91
+
92
+ def _extract_functions(self) -> List[Dict[str, Any]]:
93
+ """Extract function definitions."""
94
+ functions = []
95
+
96
+ # Common function node types
97
+ function_types = [
98
+ 'function_definition', # Python
99
+ 'function_declaration', # Go, C, JavaScript
100
+ 'function_item', # Rust
101
+ 'method_declaration', # Java, C#
102
+ 'function', # Generic
103
+ ]
104
+
105
+ for func_type in function_types:
106
+ nodes = self._find_nodes_by_type(func_type)
107
+ for node in nodes:
108
+ name = self._get_function_name(node)
109
+ if name:
110
+ functions.append({
111
+ 'line': node.start_point[0] + 1,
112
+ 'name': name,
113
+ 'signature': self._get_signature(node),
114
+ })
115
+
116
+ return functions
117
+
118
+ def _extract_classes(self) -> List[Dict[str, Any]]:
119
+ """Extract class definitions."""
120
+ classes = []
121
+
122
+ class_types = [
123
+ 'class_definition', # Python
124
+ 'class_declaration', # Java, C#, JavaScript
125
+ 'struct_item', # Rust (treated as class)
126
+ ]
127
+
128
+ for class_type in class_types:
129
+ nodes = self._find_nodes_by_type(class_type)
130
+ for node in nodes:
131
+ name = self._get_class_name(node)
132
+ if name:
133
+ classes.append({
134
+ 'line': node.start_point[0] + 1,
135
+ 'name': name,
136
+ })
137
+
138
+ return classes
139
+
140
+ def _extract_structs(self) -> List[Dict[str, Any]]:
141
+ """Extract struct definitions (for languages that have them)."""
142
+ structs = []
143
+
144
+ struct_types = [
145
+ 'struct_item', # Rust
146
+ 'struct_specifier', # C/C++
147
+ 'struct_declaration', # Go
148
+ ]
149
+
150
+ for struct_type in struct_types:
151
+ nodes = self._find_nodes_by_type(struct_type)
152
+ for node in nodes:
153
+ name = self._get_struct_name(node)
154
+ if name:
155
+ structs.append({
156
+ 'line': node.start_point[0] + 1,
157
+ 'name': name,
158
+ })
159
+
160
+ return structs
161
+
162
+ def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
163
+ """Extract a specific element using tree-sitter.
164
+
165
+ Args:
166
+ element_type: 'function', 'class', 'struct', etc.
167
+ name: Name of the element
168
+
169
+ Returns:
170
+ Dict with source, line numbers, etc.
171
+ """
172
+ if not self.tree:
173
+ return super().extract_element(element_type, name)
174
+
175
+ # Map element type to node types
176
+ type_map = {
177
+ 'function': ['function_definition', 'function_declaration', 'function_item', 'method_declaration'],
178
+ 'class': ['class_definition', 'class_declaration'],
179
+ 'struct': ['struct_item', 'struct_specifier', 'struct_declaration'],
180
+ }
181
+
182
+ node_types = type_map.get(element_type, [element_type])
183
+
184
+ # Find matching node
185
+ for node_type in node_types:
186
+ nodes = self._find_nodes_by_type(node_type)
187
+ for node in nodes:
188
+ node_name = self._get_node_name(node)
189
+ if node_name == name:
190
+ return {
191
+ 'name': name,
192
+ 'line_start': node.start_point[0] + 1,
193
+ 'line_end': node.end_point[0] + 1,
194
+ 'source': self._get_node_text(node),
195
+ }
196
+
197
+ # Fall back to grep
198
+ return super().extract_element(element_type, name)
199
+
200
+ def _find_nodes_by_type(self, node_type: str) -> List:
201
+ """Find all nodes of a given type in the tree."""
202
+ if not self.tree:
203
+ return []
204
+
205
+ nodes = []
206
+
207
+ def walk(node):
208
+ if node.type == node_type:
209
+ nodes.append(node)
210
+ for child in node.children:
211
+ walk(child)
212
+
213
+ walk(self.tree.root_node)
214
+ return nodes
215
+
216
+ def _get_node_text(self, node) -> str:
217
+ """Get the source text for a node.
218
+
219
+ IMPORTANT: Tree-sitter uses byte offsets, not character offsets!
220
+ Must slice the UTF-8 bytes, not the string, to handle multi-byte characters.
221
+ """
222
+ start_byte = node.start_byte
223
+ end_byte = node.end_byte
224
+ # Convert to bytes, slice, then decode back to string
225
+ content_bytes = self.content.encode('utf-8')
226
+ return content_bytes[start_byte:end_byte].decode('utf-8')
227
+
228
+ def _get_node_name(self, node) -> Optional[str]:
229
+ """Get the name of a node (function/class/struct name)."""
230
+ # Look for 'name' or 'identifier' child
231
+ for child in node.children:
232
+ if child.type in ('identifier', 'name'):
233
+ return self._get_node_text(child)
234
+
235
+ return None
236
+
237
+ def _get_function_name(self, node) -> Optional[str]:
238
+ """Extract function name from function node."""
239
+ return self._get_node_name(node)
240
+
241
+ def _get_class_name(self, node) -> Optional[str]:
242
+ """Extract class name from class node."""
243
+ return self._get_node_name(node)
244
+
245
+ def _get_struct_name(self, node) -> Optional[str]:
246
+ """Extract struct name from struct node."""
247
+ return self._get_node_name(node)
248
+
249
+ def _get_signature(self, node) -> str:
250
+ """Get function signature (parameters and return type only)."""
251
+ # Look for parameters node to extract just signature part
252
+ params_text = ''
253
+ return_type = ''
254
+
255
+ for child in node.children:
256
+ if child.type in ('parameters', 'parameter_list', 'formal_parameters'):
257
+ params_text = self._get_node_text(child)
258
+ elif child.type in ('return_type', 'type'):
259
+ return_type = ' -> ' + self._get_node_text(child).strip(': ')
260
+
261
+ if params_text:
262
+ return params_text + return_type
263
+
264
+ # Fallback: try to extract from first line
265
+ text = self._get_node_text(node)
266
+ first_line = text.split('\n')[0].strip()
267
+
268
+ # Remove common prefixes (def, func, fn, function, etc.)
269
+ for prefix in ['def ', 'func ', 'fn ', 'function ', 'async def ', 'pub fn ', 'fn ', 'async fn ']:
270
+ if first_line.startswith(prefix):
271
+ first_line = first_line[len(prefix):]
272
+ break
273
+
274
+ # Extract just the signature part (name + params + return)
275
+ # Remove the name to leave just params + return type
276
+ if '(' in first_line:
277
+ name_end = first_line.index('(')
278
+ signature = first_line[name_end:].rstrip(':').strip()
279
+ return signature
280
+
281
+ return first_line