reveal-cli 0.2.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.
Files changed (51) hide show
  1. plugins/c-header.yaml +89 -0
  2. plugins/gdscript.yaml +96 -0
  3. plugins/python.yaml +94 -0
  4. plugins/yaml.yaml +87 -0
  5. reveal/__init__.py +20 -0
  6. reveal/_archive_old_v0.1/analyzers/__init__.py +39 -0
  7. reveal/_archive_old_v0.1/analyzers/base.py +143 -0
  8. reveal/_archive_old_v0.1/analyzers/bash_analyzer.py +202 -0
  9. reveal/_archive_old_v0.1/analyzers/csharp_analyzer.py +167 -0
  10. reveal/_archive_old_v0.1/analyzers/gdscript_analyzer.py +201 -0
  11. reveal/_archive_old_v0.1/analyzers/go_analyzer.py +230 -0
  12. reveal/_archive_old_v0.1/analyzers/javascript_analyzer.py +272 -0
  13. reveal/_archive_old_v0.1/analyzers/json_analyzer.py +143 -0
  14. reveal/_archive_old_v0.1/analyzers/markdown_analyzer.py +116 -0
  15. reveal/_archive_old_v0.1/analyzers/php_analyzer.py +166 -0
  16. reveal/_archive_old_v0.1/analyzers/python_analyzer.py +118 -0
  17. reveal/_archive_old_v0.1/analyzers/rust_analyzer.py +202 -0
  18. reveal/_archive_old_v0.1/analyzers/sql_analyzer.py +534 -0
  19. reveal/_archive_old_v0.1/analyzers/text_analyzer.py +35 -0
  20. reveal/_archive_old_v0.1/analyzers/toml_analyzer.py +137 -0
  21. reveal/_archive_old_v0.1/analyzers/treesitter_base.py +276 -0
  22. reveal/_archive_old_v0.1/analyzers/yaml_analyzer.py +127 -0
  23. reveal/_archive_old_v0.1/breadcrumbs.py +147 -0
  24. reveal/_archive_old_v0.1/cli.py +445 -0
  25. reveal/_archive_old_v0.1/core.py +136 -0
  26. reveal/_archive_old_v0.1/detectors.py +29 -0
  27. reveal/_archive_old_v0.1/formatters.py +272 -0
  28. reveal/_archive_old_v0.1/grep_filter.py +85 -0
  29. reveal/_archive_old_v0.1/plugin_loader.py +178 -0
  30. reveal/_archive_old_v0.1/registry.py +286 -0
  31. reveal/analyzers/__init__.py +23 -0
  32. reveal/analyzers/go.py +13 -0
  33. reveal/analyzers/jupyter_analyzer.py +226 -0
  34. reveal/analyzers/markdown.py +79 -0
  35. reveal/analyzers/python.py +15 -0
  36. reveal/analyzers/rust.py +13 -0
  37. reveal/analyzers/yaml_json.py +110 -0
  38. reveal/base.py +193 -0
  39. reveal/main.py +216 -0
  40. reveal/tests/__init__.py +1 -0
  41. reveal/tests/test_json_yaml_line_numbers.py +238 -0
  42. reveal/tests/test_line_numbers.py +151 -0
  43. reveal/tests/test_toml_analyzer.py +220 -0
  44. reveal/tree_view.py +105 -0
  45. reveal/treesitter.py +282 -0
  46. reveal_cli-0.2.0.dist-info/METADATA +319 -0
  47. reveal_cli-0.2.0.dist-info/RECORD +51 -0
  48. reveal_cli-0.2.0.dist-info/WHEEL +5 -0
  49. reveal_cli-0.2.0.dist-info/entry_points.txt +2 -0
  50. reveal_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  51. reveal_cli-0.2.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,116 @@
1
+ """Markdown file analyzer."""
2
+
3
+ import re
4
+ from typing import Dict, Any, List
5
+ from .base import BaseAnalyzer
6
+ from ..registry import register
7
+
8
+
9
+ @register(['.md', '.markdown', '.mdown'], name='Markdown', icon='📝')
10
+ class MarkdownAnalyzer(BaseAnalyzer):
11
+ """Analyzer for Markdown documentation files"""
12
+
13
+ def analyze_structure(self) -> Dict[str, Any]:
14
+ """Analyze Markdown structure."""
15
+ headings = []
16
+ paragraph_count = 0
17
+ code_block_count = 0
18
+ list_count = 0
19
+
20
+ in_code_block = False
21
+ current_paragraph = False
22
+
23
+ for line in self.lines:
24
+ stripped = line.strip()
25
+
26
+ # Code blocks
27
+ if stripped.startswith('```'):
28
+ in_code_block = not in_code_block
29
+ if in_code_block:
30
+ code_block_count += 1
31
+ continue
32
+
33
+ if in_code_block:
34
+ continue
35
+
36
+ # Headings
37
+ if stripped.startswith('#'):
38
+ match = re.match(r'^(#{1,3})\s+(.+)$', stripped)
39
+ if match:
40
+ level = len(match.group(1))
41
+ title = match.group(2)
42
+ headings.append((level, title))
43
+ current_paragraph = False
44
+ continue
45
+
46
+ # Lists
47
+ if re.match(r'^\s*[-*+]\s+', line) or re.match(r'^\s*\d+\.\s+', line):
48
+ list_count += 1
49
+ current_paragraph = False
50
+ continue
51
+
52
+ # Paragraphs
53
+ if stripped and not current_paragraph:
54
+ paragraph_count += 1
55
+ current_paragraph = True
56
+ elif not stripped:
57
+ current_paragraph = False
58
+
59
+ return {
60
+ 'headings': headings,
61
+ 'paragraph_count': paragraph_count,
62
+ 'code_block_count': code_block_count,
63
+ 'list_count': list_count
64
+ }
65
+
66
+ def generate_preview(self) -> List[tuple[int, str]]:
67
+ """Generate Markdown preview."""
68
+ preview = []
69
+ in_frontmatter = False
70
+ frontmatter_start = False
71
+ first_heading_found = False
72
+ lines_after_heading = 0
73
+ in_code_block = False
74
+
75
+ for i, line in enumerate(self.lines, 1):
76
+ stripped = line.strip()
77
+
78
+ # Frontmatter detection
79
+ if i == 1 and stripped == '---':
80
+ in_frontmatter = True
81
+ frontmatter_start = True
82
+ preview.append((i, line))
83
+ continue
84
+
85
+ if in_frontmatter:
86
+ preview.append((i, line))
87
+ if stripped == '---':
88
+ in_frontmatter = False
89
+ continue
90
+
91
+ # Code blocks
92
+ if stripped.startswith('```'):
93
+ in_code_block = not in_code_block
94
+ if in_code_block:
95
+ preview.append((i, f"[Code block: {stripped[3:] or 'unknown'}]"))
96
+ continue
97
+
98
+ if in_code_block:
99
+ continue
100
+
101
+ # First heading + paragraph
102
+ if stripped.startswith('#'):
103
+ preview.append((i, line))
104
+ first_heading_found = True
105
+ lines_after_heading = 0
106
+ continue
107
+
108
+ if first_heading_found and lines_after_heading < 5:
109
+ if stripped:
110
+ preview.append((i, line))
111
+ lines_after_heading += 1
112
+
113
+ if len(preview) >= 20:
114
+ break
115
+
116
+ return preview[:30]
@@ -0,0 +1,166 @@
1
+ """
2
+ PHP file analyzer using tree-sitter.
3
+
4
+ Extracts:
5
+ - Classes
6
+ - Functions
7
+ - Methods
8
+ - Traits
9
+ - Interfaces
10
+ - Namespaces
11
+ """
12
+
13
+ from typing import Dict, Any, List
14
+ from .treesitter_base import TreeSitterAnalyzer, TREE_SITTER_AVAILABLE
15
+ from ..registry import register
16
+
17
+
18
+ @register(['.php'], name='PHP', icon='🐘')
19
+ class PHPAnalyzer(TreeSitterAnalyzer):
20
+ """
21
+ Analyzer for PHP source files.
22
+
23
+ Features:
24
+ - Classes, interfaces, traits
25
+ - Functions and methods
26
+ - Namespaces
27
+ - Use statements
28
+
29
+ All positions are exact (tree-sitter preserves source locations).
30
+ """
31
+
32
+ language_name = 'php'
33
+
34
+ def __init__(self, lines: List[str], **kwargs):
35
+ """Initialize PHP analyzer."""
36
+ self.node_type_map = {
37
+ 'function': 'function_definition',
38
+ 'class': 'class_declaration',
39
+ 'import': 'namespace_use_declaration',
40
+ }
41
+ super().__init__(lines, **kwargs)
42
+
43
+ def extract_custom(self) -> Dict[str, List[Dict[str, Any]]]:
44
+ """Extract PHP-specific items."""
45
+ return {
46
+ 'methods': self._extract_nodes('method_declaration', name_field='name'),
47
+ 'interfaces': self._extract_nodes('interface_declaration', name_field='name'),
48
+ 'traits': self._extract_nodes('trait_declaration', name_field='name'),
49
+ 'namespaces': self._extract_namespaces(),
50
+ }
51
+
52
+ def _extract_namespaces(self) -> List[Dict[str, Any]]:
53
+ """Extract namespace declarations."""
54
+ if not self.tree:
55
+ return []
56
+
57
+ namespaces = []
58
+ cursor = self.tree.walk()
59
+
60
+ def visit(cursor):
61
+ node = cursor.node
62
+
63
+ if node.type == 'namespace_definition':
64
+ # Get namespace name
65
+ name_node = node.child_by_field_name('name')
66
+ if name_node:
67
+ line = node.start_point[0] + 1
68
+ if self.in_focus_range(line):
69
+ namespaces.append({
70
+ 'name': name_node.text.decode('utf-8'),
71
+ 'line': line
72
+ })
73
+
74
+ # Recurse
75
+ if cursor.goto_first_child():
76
+ visit(cursor)
77
+ while cursor.goto_next_sibling():
78
+ visit(cursor)
79
+ cursor.goto_parent()
80
+
81
+ visit(cursor)
82
+ return namespaces
83
+
84
+ def format_structure(self, structure: Dict[str, Any]) -> List[str]:
85
+ """
86
+ Custom formatting for PHP structure output.
87
+
88
+ Groups items logically:
89
+ 1. Namespaces
90
+ 2. Use statements
91
+ 3. Types (classes, interfaces, traits)
92
+ 4. Functions
93
+ 5. Methods
94
+ """
95
+ if not TREE_SITTER_AVAILABLE or 'error' in structure:
96
+ return None
97
+
98
+ lines = []
99
+
100
+ # Namespaces
101
+ namespaces = structure.get('namespaces', [])
102
+ if namespaces:
103
+ lines.append(f"\nNamespaces ({len(namespaces)}):")
104
+ for item in namespaces:
105
+ loc = self.format_location(item['line'])
106
+ lines.append(f" {loc:30} {item['name']}")
107
+
108
+ # Use statements
109
+ imports = structure.get('imports', [])
110
+ if imports:
111
+ lines.append(f"\nUse statements ({len(imports)}):")
112
+ for item in imports[:10]:
113
+ loc = self.format_location(item['line'])
114
+ name = item['name']
115
+ if len(name) > 60:
116
+ name = name[:57] + "..."
117
+ lines.append(f" {loc:30} {name}")
118
+ if len(imports) > 10:
119
+ lines.append(f" ... and {len(imports) - 10} more")
120
+
121
+ # Types
122
+ classes = structure.get('classes', [])
123
+ interfaces = structure.get('interfaces', [])
124
+ traits = structure.get('traits', [])
125
+
126
+ type_count = len(classes) + len(interfaces) + len(traits)
127
+ if type_count > 0:
128
+ lines.append(f"\nTypes ({type_count}):")
129
+
130
+ if classes:
131
+ lines.append(f" Classes ({len(classes)}):")
132
+ for item in classes:
133
+ loc = self.format_location(item['line'])
134
+ lines.append(f" {loc:28} {item['name']}")
135
+
136
+ if interfaces:
137
+ lines.append(f" Interfaces ({len(interfaces)}):")
138
+ for item in interfaces:
139
+ loc = self.format_location(item['line'])
140
+ lines.append(f" {loc:28} {item['name']}")
141
+
142
+ if traits:
143
+ lines.append(f" Traits ({len(traits)}):")
144
+ for item in traits:
145
+ loc = self.format_location(item['line'])
146
+ lines.append(f" {loc:28} {item['name']}")
147
+
148
+ # Functions
149
+ functions = structure.get('functions', [])
150
+ if functions:
151
+ lines.append(f"\nFunctions ({len(functions)}):")
152
+ for item in functions:
153
+ loc = self.format_location(item['line'])
154
+ lines.append(f" {loc:30} function {item['name']}()")
155
+
156
+ # Methods
157
+ methods = structure.get('methods', [])
158
+ if methods:
159
+ lines.append(f"\nMethods ({len(methods)}):")
160
+ for item in methods[:20]:
161
+ loc = self.format_location(item['line'])
162
+ lines.append(f" {loc:30} {item['name']}()")
163
+ if len(methods) > 20:
164
+ lines.append(f" ... and {len(methods) - 20} more")
165
+
166
+ return lines if lines else None
@@ -0,0 +1,118 @@
1
+ """Python file analyzer."""
2
+
3
+ import ast
4
+ from typing import Dict, Any, List
5
+ from .base import BaseAnalyzer
6
+ from ..registry import register
7
+
8
+
9
+ @register(['.py', '.pyx', '.pyi'], name='Python', icon='🐍')
10
+ class PythonAnalyzer(BaseAnalyzer):
11
+ """Analyzer for Python source files with AST-based analysis"""
12
+
13
+ def __init__(self, lines: List[str], **kwargs):
14
+ super().__init__(lines, **kwargs)
15
+ self.parse_error = None
16
+ self.tree = None
17
+
18
+ try:
19
+ content = '\n'.join(lines)
20
+ self.tree = ast.parse(content)
21
+ except Exception as e:
22
+ self.parse_error = str(e)
23
+
24
+ def analyze_structure(self) -> Dict[str, Any]:
25
+ """Analyze Python structure."""
26
+ if self.parse_error:
27
+ return {
28
+ 'error': self.parse_error,
29
+ 'imports': [],
30
+ 'classes': [],
31
+ 'functions': [],
32
+ 'assignments': []
33
+ }
34
+
35
+ imports = []
36
+ classes = []
37
+ functions = []
38
+ assignments = []
39
+
40
+ for node in ast.walk(self.tree):
41
+ if isinstance(node, ast.Import):
42
+ for alias in node.names:
43
+ imports.append(alias.name)
44
+ elif isinstance(node, ast.ImportFrom):
45
+ module = node.module or ''
46
+ for alias in node.names:
47
+ imports.append(f"{module}.{alias.name}" if module else alias.name)
48
+
49
+ # Only top-level elements
50
+ for node in self.tree.body:
51
+ if isinstance(node, ast.ClassDef):
52
+ classes.append({'name': node.name, 'line': node.lineno})
53
+ elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
54
+ functions.append({'name': node.name, 'line': node.lineno})
55
+ elif isinstance(node, ast.Assign):
56
+ for target in node.targets:
57
+ if isinstance(target, ast.Name):
58
+ assignments.append({'name': target.id, 'line': node.lineno})
59
+
60
+ return {
61
+ 'imports': imports,
62
+ 'classes': classes,
63
+ 'functions': functions,
64
+ 'assignments': assignments
65
+ }
66
+
67
+ def generate_preview(self) -> List[tuple[int, str]]:
68
+ """Generate Python preview."""
69
+ preview = []
70
+
71
+ # Module docstring
72
+ if self.tree and isinstance(self.tree.body[0], ast.Expr) and isinstance(self.tree.body[0].value, ast.Constant):
73
+ docstring_node = self.tree.body[0]
74
+ # Find docstring in lines
75
+ for i, line in enumerate(self.lines[:20], 1):
76
+ if '"""' in line or "'''" in line:
77
+ preview.append((i, line))
78
+ # Add next few lines if multiline docstring
79
+ if line.count('"""') == 1 or line.count("'''") == 1:
80
+ for j in range(i, min(i + 10, len(self.lines) + 1)):
81
+ if j > i:
82
+ preview.append((j, self.lines[j - 1]))
83
+ if '"""' in self.lines[j - 1][1:] or "'''" in self.lines[j - 1][1:]:
84
+ break
85
+ break
86
+
87
+ if self.parse_error:
88
+ # Fallback to first 20 lines
89
+ for i, line in enumerate(self.lines[:20], 1):
90
+ if (i, line) not in preview:
91
+ preview.append((i, line))
92
+ return preview
93
+
94
+ # Class signatures + first docstring line
95
+ for node in self.tree.body:
96
+ if isinstance(node, ast.ClassDef):
97
+ class_line = node.lineno
98
+ preview.append((class_line, self.lines[class_line - 1]))
99
+
100
+ # Get class docstring
101
+ if node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant):
102
+ doc_line = node.body[0].lineno
103
+ preview.append((doc_line, self.lines[doc_line - 1]))
104
+
105
+ # Function signatures + first docstring line
106
+ for node in self.tree.body:
107
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
108
+ func_line = node.lineno
109
+ preview.append((func_line, self.lines[func_line - 1]))
110
+
111
+ # Get function docstring
112
+ if node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant):
113
+ doc_line = node.body[0].lineno
114
+ preview.append((doc_line, self.lines[doc_line - 1]))
115
+
116
+ # Sort by line number and limit
117
+ preview = sorted(list(set(preview)), key=lambda x: x[0])
118
+ return preview[:30]
@@ -0,0 +1,202 @@
1
+ """
2
+ Rust file analyzer using tree-sitter.
3
+
4
+ Demonstrates how easy it is to add language support using TreeSitterAnalyzer.
5
+ This entire analyzer is ~100 lines including docs and formatting!
6
+
7
+ Extracts:
8
+ - Functions (fn)
9
+ - Structs
10
+ - Enums
11
+ - Traits
12
+ - Impls (trait implementations)
13
+ - Use statements (imports)
14
+ - Mods (module declarations)
15
+ """
16
+
17
+ from typing import Dict, Any, List
18
+ from .treesitter_base import TreeSitterAnalyzer, TREE_SITTER_AVAILABLE
19
+ from ..registry import register
20
+
21
+
22
+ @register(['.rs'], name='Rust', icon='🦀')
23
+ class RustAnalyzer(TreeSitterAnalyzer):
24
+ """
25
+ Analyzer for Rust source files.
26
+
27
+ Features:
28
+ - Functions, structs, enums, traits with line numbers
29
+ - Impl blocks (trait implementations)
30
+ - Module structure
31
+ - Use statements (imports)
32
+ - Macro definitions
33
+
34
+ All positions are exact (tree-sitter preserves source locations).
35
+ """
36
+
37
+ # Set language for tree-sitter
38
+ language_name = 'rust'
39
+
40
+ def __init__(self, lines: List[str], **kwargs):
41
+ """Initialize Rust analyzer."""
42
+ # Map reveal's generic names to Rust's tree-sitter node types
43
+ self.node_type_map = {
44
+ 'function': 'function_item',
45
+ 'struct': 'struct_item',
46
+ 'class': 'struct_item', # Rust uses structs, not classes
47
+ 'import': 'use_declaration',
48
+ }
49
+ super().__init__(lines, **kwargs)
50
+
51
+ def extract_custom(self) -> Dict[str, List[Dict[str, Any]]]:
52
+ """Extract Rust-specific items."""
53
+ return {
54
+ 'enums': self._extract_nodes('enum_item', name_field='name'),
55
+ 'traits': self._extract_nodes('trait_item', name_field='name'),
56
+ 'impls': self._extract_impl_blocks(),
57
+ 'mods': self._extract_nodes('mod_item', name_field='name'),
58
+ 'macros': self._extract_nodes('macro_definition', name_field='name'),
59
+ }
60
+
61
+ def _extract_impl_blocks(self) -> List[Dict[str, Any]]:
62
+ """
63
+ Extract impl blocks with special handling.
64
+
65
+ Rust impl blocks look like:
66
+ - impl Foo { ... } (inherent impl)
67
+ - impl Trait for Foo { ... } (trait impl)
68
+ """
69
+ if not self.tree:
70
+ return []
71
+
72
+ impls = []
73
+ cursor = self.tree.walk()
74
+
75
+ def visit(cursor):
76
+ node = cursor.node
77
+
78
+ if node.type == 'impl_item':
79
+ # Try to get type being implemented
80
+ type_node = node.child_by_field_name('type')
81
+ trait_node = node.child_by_field_name('trait')
82
+
83
+ if trait_node and type_node:
84
+ # impl Trait for Type
85
+ trait_name = trait_node.text.decode('utf-8')
86
+ type_name = type_node.text.decode('utf-8')
87
+ name = f"{trait_name} for {type_name}"
88
+ elif type_node:
89
+ # impl Type
90
+ type_name = type_node.text.decode('utf-8')
91
+ name = type_name
92
+ else:
93
+ name = "<impl>"
94
+
95
+ line = node.start_point[0] + 1
96
+ if self.in_focus_range(line):
97
+ impls.append({
98
+ 'name': name,
99
+ 'line': line
100
+ })
101
+
102
+ # Recurse
103
+ if cursor.goto_first_child():
104
+ visit(cursor)
105
+ while cursor.goto_next_sibling():
106
+ visit(cursor)
107
+ cursor.goto_parent()
108
+
109
+ visit(cursor)
110
+ return impls
111
+
112
+ def format_structure(self, structure: Dict[str, Any]) -> List[str]:
113
+ """
114
+ Custom formatting for Rust structure output.
115
+
116
+ Groups items logically:
117
+ 1. Module/Crate info
118
+ 2. Imports (use statements)
119
+ 3. Types (structs, enums, traits)
120
+ 4. Implementations
121
+ 5. Functions
122
+ 6. Macros
123
+ """
124
+ if not TREE_SITTER_AVAILABLE or 'error' in structure:
125
+ return None # Use default formatting
126
+
127
+ lines = []
128
+
129
+ # Use statements
130
+ imports = structure.get('imports', [])
131
+ if imports:
132
+ lines.append(f"\nUse statements ({len(imports)}):")
133
+ for item in imports[:10]: # Limit display
134
+ loc = self.format_location(item['line'])
135
+ # Shorten long imports
136
+ name = item['name']
137
+ if len(name) > 60:
138
+ name = name[:57] + "..."
139
+ lines.append(f" {loc:30} {name}")
140
+ if len(imports) > 10:
141
+ lines.append(f" ... and {len(imports) - 10} more")
142
+
143
+ # Types section
144
+ structs = structure.get('structs', [])
145
+ enums = structure.get('enums', [])
146
+ traits = structure.get('traits', [])
147
+
148
+ type_count = len(structs) + len(enums) + len(traits)
149
+ if type_count > 0:
150
+ lines.append(f"\nTypes ({type_count}):")
151
+
152
+ if structs:
153
+ lines.append(f" Structs ({len(structs)}):")
154
+ for item in structs:
155
+ loc = self.format_location(item['line'])
156
+ lines.append(f" {loc:28} {item['name']}")
157
+
158
+ if enums:
159
+ lines.append(f" Enums ({len(enums)}):")
160
+ for item in enums:
161
+ loc = self.format_location(item['line'])
162
+ lines.append(f" {loc:28} {item['name']}")
163
+
164
+ if traits:
165
+ lines.append(f" Traits ({len(traits)}):")
166
+ for item in traits:
167
+ loc = self.format_location(item['line'])
168
+ lines.append(f" {loc:28} {item['name']}")
169
+
170
+ # Implementations
171
+ impls = structure.get('impls', [])
172
+ if impls:
173
+ lines.append(f"\nImplementations ({len(impls)}):")
174
+ for item in impls:
175
+ loc = self.format_location(item['line'])
176
+ lines.append(f" {loc:30} impl {item['name']}")
177
+
178
+ # Functions
179
+ functions = structure.get('functions', [])
180
+ if functions:
181
+ lines.append(f"\nFunctions ({len(functions)}):")
182
+ for item in functions:
183
+ loc = self.format_location(item['line'])
184
+ lines.append(f" {loc:30} fn {item['name']}()")
185
+
186
+ # Modules
187
+ mods = structure.get('mods', [])
188
+ if mods:
189
+ lines.append(f"\nModules ({len(mods)}):")
190
+ for item in mods:
191
+ loc = self.format_location(item['line'])
192
+ lines.append(f" {loc:30} mod {item['name']}")
193
+
194
+ # Macros
195
+ macros = structure.get('macros', [])
196
+ if macros:
197
+ lines.append(f"\nMacros ({len(macros)}):")
198
+ for item in macros:
199
+ loc = self.format_location(item['line'])
200
+ lines.append(f" {loc:30} {item['name']}!")
201
+
202
+ return lines if lines else None