codegraph-nav 0.1.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 (41) hide show
  1. codegraph_nav/__init__.py +194 -0
  2. codegraph_nav/ast_grep_analyzer.py +448 -0
  3. codegraph_nav/cli.py +223 -0
  4. codegraph_nav/code_navigator.py +1328 -0
  5. codegraph_nav/code_search.py +1009 -0
  6. codegraph_nav/colors.py +209 -0
  7. codegraph_nav/completions.py +354 -0
  8. codegraph_nav/dart_analyzer.py +301 -0
  9. codegraph_nav/dependency_graph.py +814 -0
  10. codegraph_nav/domain/__init__.py +20 -0
  11. codegraph_nav/domain/routes.py +337 -0
  12. codegraph_nav/domain/schemas.py +229 -0
  13. codegraph_nav/domain/tags.py +87 -0
  14. codegraph_nav/exporters.py +563 -0
  15. codegraph_nav/go_analyzer.py +273 -0
  16. codegraph_nav/graph/__init__.py +72 -0
  17. codegraph_nav/graph/builder.py +409 -0
  18. codegraph_nav/graph/communities.py +402 -0
  19. codegraph_nav/graph/flows.py +311 -0
  20. codegraph_nav/graph/query.py +380 -0
  21. codegraph_nav/graph/schema.py +266 -0
  22. codegraph_nav/graph/search.py +257 -0
  23. codegraph_nav/graph/store.py +517 -0
  24. codegraph_nav/hints.py +195 -0
  25. codegraph_nav/import_resolver.py +891 -0
  26. codegraph_nav/js_ts_analyzer.py +564 -0
  27. codegraph_nav/line_reader.py +664 -0
  28. codegraph_nav/mcp/__init__.py +39 -0
  29. codegraph_nav/mcp/__main__.py +5 -0
  30. codegraph_nav/mcp/server.py +2228 -0
  31. codegraph_nav/py.typed +2 -0
  32. codegraph_nav/ruby_analyzer.py +259 -0
  33. codegraph_nav/rust_analyzer.py +379 -0
  34. codegraph_nav/token_efficient_renderer.py +743 -0
  35. codegraph_nav/watcher.py +382 -0
  36. codegraph_nav-0.1.0.dist-info/METADATA +487 -0
  37. codegraph_nav-0.1.0.dist-info/RECORD +41 -0
  38. codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
  39. codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
  40. codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
  41. codegraph_nav-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,563 @@
1
+ #!/usr/bin/env python3
2
+ """Export code map to different formats.
3
+
4
+ Provides exporters for converting code maps to Markdown, HTML, and GraphViz
5
+ formats for documentation and visualization purposes.
6
+
7
+ Example:
8
+ Command line usage:
9
+ $ codegraph-nav export -f markdown -o docs/codebase.md
10
+ $ codegraph-nav export -f html -o docs/codebase.html
11
+ $ codegraph-nav export -f graphviz -o docs/deps.dot
12
+
13
+ Python API usage:
14
+ >>> from codegraph_nav.exporters import MarkdownExporter
15
+ >>> exporter = MarkdownExporter('.codegraph.json')
16
+ >>> markdown = exporter.export()
17
+ """
18
+
19
+ import html
20
+ import json
21
+ import os
22
+ import sys
23
+ from abc import ABC, abstractmethod
24
+ from collections import defaultdict
25
+ from collections.abc import Callable
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from .colors import get_colors
30
+
31
+ __version__ = "0.1.0"
32
+
33
+
34
+ class BaseExporter(ABC):
35
+ """Base class for code map exporters.
36
+
37
+ Attributes:
38
+ code_map: Loaded code map data.
39
+ map_path: Path to the code map file.
40
+ """
41
+
42
+ def __init__(self, map_path: str):
43
+ """Initialize the exporter.
44
+
45
+ Args:
46
+ map_path: Path to the .codegraph.json file.
47
+ """
48
+ self.map_path = map_path
49
+ self.code_map = self._load_map()
50
+
51
+ def _load_map(self) -> dict[str, Any]:
52
+ """Load the code map from file."""
53
+ with open(self.map_path, encoding="utf-8") as f:
54
+ data: dict[str, Any] = json.load(f)
55
+ return data
56
+
57
+ @abstractmethod
58
+ def export(self) -> str:
59
+ """Export the code map to the target format.
60
+
61
+ Returns:
62
+ Exported content as a string.
63
+ """
64
+ pass
65
+
66
+ def export_to_file(self, output_path: str) -> None:
67
+ """Export the code map to a file.
68
+
69
+ Args:
70
+ output_path: Path to the output file.
71
+ """
72
+ content = self.export()
73
+ with open(output_path, "w", encoding="utf-8") as f:
74
+ f.write(content)
75
+
76
+
77
+ class MarkdownExporter(BaseExporter):
78
+ """Export code map to Markdown format.
79
+
80
+ Generates a Markdown document with:
81
+ - Overview statistics
82
+ - File listing with symbols
83
+ - Symbol index by type
84
+
85
+ Example:
86
+ >>> exporter = MarkdownExporter('.codegraph.json')
87
+ >>> print(exporter.export())
88
+ """
89
+
90
+ def export(self) -> str:
91
+ """Export to Markdown format.
92
+
93
+ Returns:
94
+ Markdown document as a string.
95
+ """
96
+ lines = []
97
+
98
+ # Header
99
+ root = self.code_map.get("root", "Unknown")
100
+ lines.append(f"# Code Map: {Path(root).name}")
101
+ lines.append("")
102
+ lines.append(f"Generated: {self.code_map.get('generated_at', 'Unknown')}")
103
+ lines.append("")
104
+
105
+ # Statistics
106
+ stats = self.code_map.get("stats", {})
107
+ lines.append("## Statistics")
108
+ lines.append("")
109
+ lines.append(f"- **Files:** {stats.get('files_processed', 0)}")
110
+ lines.append(f"- **Symbols:** {stats.get('symbols_found', 0)}")
111
+ lines.append("")
112
+
113
+ # Symbol counts by type
114
+ type_counts: defaultdict[str, int] = defaultdict(int)
115
+ for file_info in self.code_map.get("files", {}).values():
116
+ for sym in file_info.get("symbols", []):
117
+ type_counts[sym["type"]] += 1
118
+
119
+ if type_counts:
120
+ lines.append("### Symbols by Type")
121
+ lines.append("")
122
+ lines.append("| Type | Count |")
123
+ lines.append("|------|-------|")
124
+ for sym_type, count in sorted(type_counts.items()):
125
+ lines.append(f"| {sym_type} | {count} |")
126
+ lines.append("")
127
+
128
+ # Files
129
+ lines.append("## Files")
130
+ lines.append("")
131
+
132
+ files = self.code_map.get("files", {})
133
+ for file_path in sorted(files.keys()):
134
+ file_info = files[file_path]
135
+ symbols = file_info.get("symbols", [])
136
+
137
+ lines.append(f"### `{file_path}`")
138
+ lines.append("")
139
+
140
+ if symbols:
141
+ lines.append("| Symbol | Type | Lines |")
142
+ lines.append("|--------|------|-------|")
143
+ for sym in sorted(symbols, key=lambda s: s["lines"][0]):
144
+ name = sym["name"]
145
+ sym_type = sym["type"]
146
+ line_range = f"{sym['lines'][0]}-{sym['lines'][1]}"
147
+ lines.append(f"| `{name}` | {sym_type} | {line_range} |")
148
+ lines.append("")
149
+ else:
150
+ lines.append("*No symbols found*")
151
+ lines.append("")
152
+
153
+ # Symbol Index
154
+ lines.append("## Symbol Index")
155
+ lines.append("")
156
+
157
+ index = self.code_map.get("index", {})
158
+ for name in sorted(index.keys()):
159
+ entries = index[name]
160
+ lines.append(f"### `{name}`")
161
+ lines.append("")
162
+ for entry in entries:
163
+ file_path = entry["file"]
164
+ sym_type = entry["type"]
165
+ line_range = f"{entry['lines'][0]}-{entry['lines'][1]}"
166
+ lines.append(f"- **{sym_type}** in `{file_path}` (lines {line_range})")
167
+ lines.append("")
168
+
169
+ return "\n".join(lines)
170
+
171
+
172
+ class HTMLExporter(BaseExporter):
173
+ """Export code map to HTML format.
174
+
175
+ Generates an interactive HTML document with:
176
+ - Collapsible file tree
177
+ - Symbol search
178
+ - Statistics dashboard
179
+
180
+ Example:
181
+ >>> exporter = HTMLExporter('.codegraph.json')
182
+ >>> print(exporter.export())
183
+ """
184
+
185
+ def export(self) -> str:
186
+ """Export to HTML format.
187
+
188
+ Returns:
189
+ HTML document as a string.
190
+ """
191
+ root = self.code_map.get("root", "Unknown")
192
+ stats = self.code_map.get("stats", {})
193
+
194
+ # Symbol counts by type
195
+ type_counts: defaultdict[str, int] = defaultdict(int)
196
+ for file_info in self.code_map.get("files", {}).values():
197
+ for sym in file_info.get("symbols", []):
198
+ type_counts[sym["type"]] += 1
199
+
200
+ # Generate file tree HTML
201
+ files_html = self._generate_files_html()
202
+
203
+ # Generate type stats HTML
204
+ type_stats_html = ""
205
+ for sym_type, count in sorted(type_counts.items()):
206
+ type_stats_html += f'<div class="stat-item"><span class="type">{html.escape(sym_type)}</span><span class="count">{count}</span></div>'
207
+
208
+ return f"""<!DOCTYPE html>
209
+ <html lang="en">
210
+ <head>
211
+ <meta charset="UTF-8">
212
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
213
+ <title>Code Map: {html.escape(Path(root).name)}</title>
214
+ <style>
215
+ :root {{
216
+ --bg: #1a1a2e;
217
+ --bg-light: #16213e;
218
+ --text: #eee;
219
+ --text-dim: #888;
220
+ --accent: #0f3460;
221
+ --highlight: #e94560;
222
+ --success: #4ecca3;
223
+ --border: #333;
224
+ }}
225
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
226
+ body {{
227
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
228
+ background: var(--bg);
229
+ color: var(--text);
230
+ line-height: 1.6;
231
+ padding: 20px;
232
+ }}
233
+ .container {{ max-width: 1200px; margin: 0 auto; }}
234
+ h1 {{ color: var(--highlight); margin-bottom: 10px; }}
235
+ h2 {{ color: var(--success); margin: 20px 0 10px; border-bottom: 1px solid var(--border); padding-bottom: 5px; }}
236
+ .meta {{ color: var(--text-dim); font-size: 0.9em; margin-bottom: 20px; }}
237
+ .stats {{
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
240
+ gap: 15px;
241
+ margin-bottom: 30px;
242
+ }}
243
+ .stat-card {{
244
+ background: var(--bg-light);
245
+ border: 1px solid var(--border);
246
+ border-radius: 8px;
247
+ padding: 15px;
248
+ text-align: center;
249
+ }}
250
+ .stat-card .value {{ font-size: 2em; color: var(--success); font-weight: bold; }}
251
+ .stat-card .label {{ color: var(--text-dim); font-size: 0.9em; }}
252
+ .type-stats {{ display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }}
253
+ .stat-item {{
254
+ background: var(--accent);
255
+ padding: 5px 12px;
256
+ border-radius: 15px;
257
+ font-size: 0.9em;
258
+ }}
259
+ .stat-item .type {{ margin-right: 8px; }}
260
+ .stat-item .count {{ color: var(--success); font-weight: bold; }}
261
+ .search-box {{
262
+ width: 100%;
263
+ padding: 10px 15px;
264
+ font-size: 1em;
265
+ border: 1px solid var(--border);
266
+ border-radius: 5px;
267
+ background: var(--bg-light);
268
+ color: var(--text);
269
+ margin-bottom: 20px;
270
+ }}
271
+ .search-box:focus {{ outline: none; border-color: var(--highlight); }}
272
+ .file {{
273
+ background: var(--bg-light);
274
+ border: 1px solid var(--border);
275
+ border-radius: 5px;
276
+ margin-bottom: 10px;
277
+ }}
278
+ .file-header {{
279
+ padding: 10px 15px;
280
+ cursor: pointer;
281
+ display: flex;
282
+ justify-content: space-between;
283
+ align-items: center;
284
+ }}
285
+ .file-header:hover {{ background: var(--accent); }}
286
+ .file-path {{ font-family: monospace; color: var(--success); }}
287
+ .file-count {{ color: var(--text-dim); font-size: 0.9em; }}
288
+ .file-content {{ display: none; padding: 0 15px 15px; }}
289
+ .file.open .file-content {{ display: block; }}
290
+ .file.open .file-header {{ border-bottom: 1px solid var(--border); }}
291
+ .symbol {{
292
+ display: flex;
293
+ justify-content: space-between;
294
+ padding: 8px 0;
295
+ border-bottom: 1px solid var(--border);
296
+ }}
297
+ .symbol:last-child {{ border-bottom: none; }}
298
+ .symbol-name {{ font-family: monospace; color: var(--text); }}
299
+ .symbol-type {{
300
+ background: var(--accent);
301
+ padding: 2px 8px;
302
+ border-radius: 10px;
303
+ font-size: 0.8em;
304
+ color: var(--highlight);
305
+ }}
306
+ .symbol-lines {{ color: var(--text-dim); font-size: 0.9em; margin-left: 10px; }}
307
+ .hidden {{ display: none !important; }}
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div class="container">
312
+ <h1>Code Map: {html.escape(Path(root).name)}</h1>
313
+ <p class="meta">Generated: {html.escape(self.code_map.get('generated_at', 'Unknown'))}</p>
314
+
315
+ <div class="stats">
316
+ <div class="stat-card">
317
+ <div class="value">{stats.get('files_processed', 0)}</div>
318
+ <div class="label">Files</div>
319
+ </div>
320
+ <div class="stat-card">
321
+ <div class="value">{stats.get('symbols_found', 0)}</div>
322
+ <div class="label">Symbols</div>
323
+ </div>
324
+ </div>
325
+
326
+ <h2>Symbols by Type</h2>
327
+ <div class="type-stats">{type_stats_html}</div>
328
+
329
+ <h2>Files</h2>
330
+ <input type="text" class="search-box" placeholder="Search symbols..." id="search">
331
+ <div id="files">{files_html}</div>
332
+ </div>
333
+
334
+ <script>
335
+ // Toggle file expansion
336
+ document.querySelectorAll('.file-header').forEach(header => {{
337
+ header.addEventListener('click', () => {{
338
+ header.parentElement.classList.toggle('open');
339
+ }});
340
+ }});
341
+
342
+ // Search functionality
343
+ const searchBox = document.getElementById('search');
344
+ const files = document.querySelectorAll('.file');
345
+
346
+ searchBox.addEventListener('input', (e) => {{
347
+ const query = e.target.value.toLowerCase();
348
+ files.forEach(file => {{
349
+ const symbols = file.querySelectorAll('.symbol');
350
+ let hasMatch = false;
351
+ symbols.forEach(symbol => {{
352
+ const name = symbol.querySelector('.symbol-name').textContent.toLowerCase();
353
+ if (name.includes(query)) {{
354
+ symbol.classList.remove('hidden');
355
+ hasMatch = true;
356
+ }} else {{
357
+ symbol.classList.add('hidden');
358
+ }}
359
+ }});
360
+ if (query && hasMatch) {{
361
+ file.classList.add('open');
362
+ file.classList.remove('hidden');
363
+ }} else if (query && !hasMatch) {{
364
+ file.classList.add('hidden');
365
+ }} else {{
366
+ file.classList.remove('hidden');
367
+ symbols.forEach(s => s.classList.remove('hidden'));
368
+ }}
369
+ }});
370
+ }});
371
+ </script>
372
+ </body>
373
+ </html>"""
374
+
375
+ def _generate_files_html(self) -> str:
376
+ """Generate HTML for files section."""
377
+ files_html = []
378
+ files = self.code_map.get("files", {})
379
+
380
+ for file_path in sorted(files.keys()):
381
+ file_info = files[file_path]
382
+ symbols = file_info.get("symbols", [])
383
+
384
+ symbols_html = ""
385
+ for sym in sorted(symbols, key=lambda s: s["lines"][0]):
386
+ name = html.escape(sym["name"])
387
+ sym_type = html.escape(sym["type"])
388
+ lines = f"{sym['lines'][0]}-{sym['lines'][1]}"
389
+ symbols_html += f"""
390
+ <div class="symbol">
391
+ <span>
392
+ <span class="symbol-name">{name}</span>
393
+ <span class="symbol-lines">:{lines}</span>
394
+ </span>
395
+ <span class="symbol-type">{sym_type}</span>
396
+ </div>"""
397
+
398
+ file_html = f"""
399
+ <div class="file">
400
+ <div class="file-header">
401
+ <span class="file-path">{html.escape(file_path)}</span>
402
+ <span class="file-count">{len(symbols)} symbols</span>
403
+ </div>
404
+ <div class="file-content">{symbols_html}</div>
405
+ </div>"""
406
+ files_html.append(file_html)
407
+
408
+ return "".join(files_html)
409
+
410
+
411
+ class GraphVizExporter(BaseExporter):
412
+ """Export code map dependencies to GraphViz DOT format.
413
+
414
+ Generates a DOT graph showing:
415
+ - Symbols as nodes
416
+ - Dependencies as edges
417
+ - Files as clusters
418
+
419
+ Example:
420
+ >>> exporter = GraphVizExporter('.codegraph.json')
421
+ >>> print(exporter.export())
422
+ """
423
+
424
+ def export(self) -> str:
425
+ """Export to GraphViz DOT format.
426
+
427
+ Returns:
428
+ DOT graph as a string.
429
+ """
430
+ lines = []
431
+ lines.append("digraph CodeMap {")
432
+ lines.append(" rankdir=LR;")
433
+ lines.append(" node [shape=box, style=filled, fontname=Helvetica];")
434
+ lines.append(" edge [color=gray60];")
435
+ lines.append("")
436
+
437
+ # Color scheme for symbol types
438
+ type_colors = {
439
+ "function": "#4ecca3",
440
+ "class": "#e94560",
441
+ "method": "#0f3460",
442
+ "interface": "#ff6b6b",
443
+ "struct": "#ffa502",
444
+ "enum": "#a29bfe",
445
+ "type": "#fd79a8",
446
+ }
447
+
448
+ # Group symbols by file
449
+ files = self.code_map.get("files", {})
450
+ node_ids = {} # Map (file, name) to node id
451
+ edges = []
452
+
453
+ for file_idx, (file_path, file_info) in enumerate(sorted(files.items())):
454
+ symbols = file_info.get("symbols", [])
455
+ if not symbols:
456
+ continue
457
+
458
+ # Create subgraph (cluster) for file
459
+ cluster_name = file_path.replace("/", "_").replace(".", "_").replace("-", "_")
460
+ lines.append(f" subgraph cluster_{cluster_name} {{")
461
+ lines.append(f' label="{self._escape_dot(file_path)}";')
462
+ lines.append(" style=rounded;")
463
+ lines.append(" bgcolor=gray95;")
464
+ lines.append("")
465
+
466
+ for sym_idx, sym in enumerate(symbols):
467
+ node_id = f"node_{file_idx}_{sym_idx}"
468
+ node_ids[(file_path, sym["name"])] = node_id
469
+
470
+ color = type_colors.get(sym["type"], "#dfe6e9")
471
+ label = f"{self._escape_dot(sym['name'])}\\n[{self._escape_dot(sym['type'])}]"
472
+
473
+ lines.append(f' {node_id} [label="{label}", fillcolor="{color}"];')
474
+
475
+ # Collect dependencies for edges
476
+ deps = sym.get("deps") or []
477
+ for dep in deps:
478
+ edges.append((node_id, dep))
479
+
480
+ lines.append(" }")
481
+ lines.append("")
482
+
483
+ # Add edges for dependencies
484
+ if edges:
485
+ lines.append(" // Dependencies")
486
+ for source_id, dep_name in edges:
487
+ # Try to find the target node
488
+ target_id = None
489
+ for (_file_path, name), nid in node_ids.items():
490
+ if name == dep_name:
491
+ target_id = nid
492
+ break
493
+
494
+ if target_id:
495
+ lines.append(f" {source_id} -> {target_id};")
496
+
497
+ lines.append("")
498
+
499
+ lines.append("}")
500
+ return "\n".join(lines)
501
+
502
+ def _escape_dot(self, text: str) -> str:
503
+ """Escape text for DOT format."""
504
+ return text.replace('"', '\\"').replace("\n", "\\n")
505
+
506
+
507
+ def get_exporter(format_type: str, map_path: str) -> BaseExporter:
508
+ """Get an exporter for the specified format.
509
+
510
+ Args:
511
+ format_type: Export format ('markdown', 'html', 'graphviz').
512
+ map_path: Path to the .codegraph.json file.
513
+
514
+ Returns:
515
+ Appropriate exporter instance.
516
+
517
+ Raises:
518
+ ValueError: If format is not supported.
519
+ """
520
+ exporters: dict[str, Callable[[str], BaseExporter]] = {
521
+ "markdown": MarkdownExporter,
522
+ "md": MarkdownExporter,
523
+ "html": HTMLExporter,
524
+ "graphviz": GraphVizExporter,
525
+ "dot": GraphVizExporter,
526
+ }
527
+
528
+ exporter_class = exporters.get(format_type.lower())
529
+ if not exporter_class:
530
+ raise ValueError(f"Unsupported format: {format_type}. Supported: markdown, html, graphviz")
531
+
532
+ return exporter_class(map_path)
533
+
534
+
535
+ def run_export(args) -> None:
536
+ """Run the export command.
537
+
538
+ Args:
539
+ args: Parsed command-line arguments.
540
+ """
541
+ c = get_colors(no_color=getattr(args, "no_color", False))
542
+
543
+ map_path = getattr(args, "map", ".codegraph.json")
544
+ if not os.path.exists(map_path):
545
+ print(c.error(f"Code map not found: {map_path}"), file=sys.stderr)
546
+ sys.exit(1)
547
+
548
+ format_type = getattr(args, "format", "markdown")
549
+ output_path = getattr(args, "output", None)
550
+
551
+ try:
552
+ exporter = get_exporter(format_type, map_path)
553
+ content = exporter.export()
554
+
555
+ if output_path:
556
+ exporter.export_to_file(output_path)
557
+ print(c.success(f"✓ Exported to {output_path}"), file=sys.stderr)
558
+ else:
559
+ print(content)
560
+
561
+ except Exception as e:
562
+ print(c.error(f"Export error: {e}"), file=sys.stderr)
563
+ sys.exit(1)