Coverage for openhcs/textual_tui/windows/debug_class_explorer.py: 0.0%

382 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1"""Dynamic Visual AST Inspector for OpenHCS debugging.""" 

2 

3import ast 

4import inspect 

5import importlib 

6import pkgutil 

7from typing import Dict, List, Any, Optional, Type, Union 

8from pathlib import Path 

9from textual.app import ComposeResult 

10from textual.widgets import Static, Tree, Button, Select, TextArea, Input, Label 

11from textual.containers import Horizontal, Vertical, ScrollableContainer 

12from textual.reactive import reactive 

13 

14from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow 

15 

16 

17class ASTNodeInfo: 

18 """Information about an AST node.""" 

19 

20 def __init__(self, node: ast.AST, parent: Optional['ASTNodeInfo'] = None): 

21 self.node = node 

22 self.parent = parent 

23 self.children = [] 

24 self.node_type = type(node).__name__ 

25 self.description = self._get_description() 

26 self.details = self._get_details() 

27 

28 def _get_description(self) -> str: 

29 """Get a human-readable description of the node.""" 

30 if isinstance(self.node, ast.ClassDef): 

31 bases = [self._get_name(base) for base in self.node.bases] 

32 base_str = f"({', '.join(bases)})" if bases else "" 

33 return f"Class: {self.node.name}{base_str}" 

34 elif isinstance(self.node, ast.FunctionDef): 

35 args = [arg.arg for arg in self.node.args.args] 

36 return f"Function: {self.node.name}({', '.join(args)})" 

37 elif isinstance(self.node, ast.AsyncFunctionDef): 

38 args = [arg.arg for arg in self.node.args.args] 

39 return f"Async Function: {self.node.name}({', '.join(args)})" 

40 elif isinstance(self.node, ast.Import): 

41 names = [alias.name for alias in self.node.names] 

42 return f"Import: {', '.join(names)}" 

43 elif isinstance(self.node, ast.ImportFrom): 

44 names = [alias.name for alias in self.node.names] 

45 return f"From {self.node.module}: {', '.join(names)}" 

46 elif isinstance(self.node, ast.Assign): 

47 targets = [self._get_name(target) for target in self.node.targets] 

48 return f"Assignment: {', '.join(targets)}" 

49 elif isinstance(self.node, ast.AnnAssign): 

50 target = self._get_name(self.node.target) 

51 return f"Annotated Assignment: {target}" 

52 elif isinstance(self.node, ast.Expr): 

53 return f"Expression: {self._get_name(self.node.value)}" 

54 else: 

55 return f"{self.node_type}" 

56 

57 def _get_details(self) -> Dict[str, Any]: 

58 """Get detailed information about the node.""" 

59 details = {'type': self.node_type} 

60 

61 if isinstance(self.node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): 

62 details['name'] = self.node.name 

63 details['docstring'] = ast.get_docstring(self.node) 

64 

65 if isinstance(self.node, ast.ClassDef): 

66 details['bases'] = [self._get_name(base) for base in self.node.bases] 

67 details['decorators'] = [self._get_name(dec) for dec in self.node.decorator_list] 

68 

69 if isinstance(self.node, (ast.FunctionDef, ast.AsyncFunctionDef)): 

70 details['args'] = [arg.arg for arg in self.node.args.args] 

71 details['decorators'] = [self._get_name(dec) for dec in self.node.decorator_list] 

72 details['returns'] = self._get_name(self.node.returns) if self.node.returns else None 

73 

74 if hasattr(self.node, 'lineno'): 

75 details['line'] = self.node.lineno 

76 

77 return details 

78 

79 def _get_name(self, node: ast.AST) -> str: 

80 """Get the name of a node.""" 

81 if isinstance(node, ast.Name): 

82 return node.id 

83 elif isinstance(node, ast.Attribute): 

84 return f"{self._get_name(node.value)}.{node.attr}" 

85 elif isinstance(node, ast.Constant): 

86 return repr(node.value) 

87 elif isinstance(node, ast.Call): 

88 func_name = self._get_name(node.func) 

89 return f"{func_name}(...)" 

90 else: 

91 return f"<{type(node).__name__}>" 

92 

93 

94class DynamicASTAnalyzer: 

95 """Dynamic AST analyzer that can inspect any Python module or file.""" 

96 

97 @staticmethod 

98 def get_inspection_categories() -> Dict[str, Dict[str, str]]: 

99 """Get predefined categories of things to inspect.""" 

100 return { 

101 "Config Classes": { 

102 "config_form": "openhcs.textual_tui.widgets.config_form", 

103 "config_dialog": "openhcs.textual_tui.screens.config_dialog", 

104 "config_window": "openhcs.textual_tui.windows.config_window", 

105 "config_form_screen": "openhcs.textual_tui.screens.config_form" 

106 }, 

107 "Core Components": { 

108 "pipeline": "openhcs.core.pipeline", 

109 "orchestrator": "openhcs.core.orchestrator.orchestrator", 

110 "step_base": "openhcs.core.step_base", 

111 "config": "openhcs.core.config" 

112 }, 

113 "TUI Widgets": { 

114 "main_content": "openhcs.textual_tui.widgets.main_content", 

115 "plate_manager": "openhcs.textual_tui.widgets.plate_manager", 

116 "function_pane": "openhcs.textual_tui.widgets.function_pane", 

117 "pipeline_editor": "openhcs.textual_tui.widgets.pipeline_editor" 

118 }, 

119 "Processing Backends": { 

120 "cupy_processor": "openhcs.processing.backends.enhance.basic_processor_cupy", 

121 "numpy_processor": "openhcs.processing.backends.enhance.basic_processor_numpy", 

122 "torch_processor": "openhcs.processing.backends.enhance.n2v2_processor_torch" 

123 }, 

124 "Window System": { 

125 "base_window": "openhcs.textual_tui.windows.base_window", 

126 "help_window": "openhcs.textual_tui.windows.help_window", 

127 "file_browser": "openhcs.textual_tui.windows.file_browser_window" 

128 } 

129 } 

130 

131 @staticmethod 

132 def analyze_module(module_name: str) -> Optional[ASTNodeInfo]: 

133 """Analyze a Python module and return AST tree.""" 

134 try: 

135 # Import the module to get its file path 

136 module = importlib.import_module(module_name) 

137 file_path = inspect.getfile(module) 

138 

139 # Read and parse the source 

140 with open(file_path, 'r') as f: 

141 source = f.read() 

142 

143 tree = ast.parse(source) 

144 return DynamicASTAnalyzer._build_ast_tree(tree) 

145 

146 except Exception as e: 

147 # Create error node 

148 error_node = ast.Module(body=[], type_ignores=[]) 

149 error_info = ASTNodeInfo(error_node) 

150 error_info.description = f"Error: {e}" 

151 return error_info 

152 

153 @staticmethod 

154 def analyze_file(file_path: str) -> Optional[ASTNodeInfo]: 

155 """Analyze a Python file and return AST tree.""" 

156 try: 

157 with open(file_path, 'r') as f: 

158 source = f.read() 

159 

160 tree = ast.parse(source) 

161 return DynamicASTAnalyzer._build_ast_tree(tree) 

162 

163 except Exception as e: 

164 # Create error node 

165 error_node = ast.Module(body=[], type_ignores=[]) 

166 error_info = ASTNodeInfo(error_node) 

167 error_info.description = f"Error: {e}" 

168 return error_info 

169 

170 @staticmethod 

171 def _build_ast_tree(node: ast.AST, parent: Optional[ASTNodeInfo] = None) -> ASTNodeInfo: 

172 """Recursively build AST tree.""" 

173 node_info = ASTNodeInfo(node, parent) 

174 

175 # Add children 

176 for child in ast.iter_child_nodes(node): 

177 child_info = DynamicASTAnalyzer._build_ast_tree(child, node_info) 

178 node_info.children.append(child_info) 

179 

180 return node_info 

181 

182 

183class DebugClassExplorerWindow(BaseOpenHCSWindow): 

184 """Dynamic Visual AST Inspector for OpenHCS debugging.""" 

185 

186 DEFAULT_CSS = """ 

187 DebugClassExplorerWindow { 

188 width: 95; height: 35; 

189 min-width: 80; min-height: 25; 

190 } 

191 

192 .category-buttons { 

193 height: auto; 

194 width: 1fr; 

195 } 

196 

197 .custom-input { 

198 height: auto; 

199 width: 1fr; 

200 } 

201 

202 .ast-tree { 

203 width: 1fr; 

204 height: 1fr; 

205 } 

206 

207 .ast-details { 

208 width: 1fr; 

209 height: 1fr; 

210 } 

211 

212 .detail-text { 

213 height: 1fr; 

214 border: solid $primary; 

215 padding: 1; 

216 } 

217 

218 .breadcrumbs { 

219 height: auto; 

220 width: 1fr; 

221 margin: 1 0; 

222 } 

223 

224 #breadcrumb_path { 

225 margin: 0 1; 

226 color: $text-muted; 

227 } 

228 

229 /* Use standard button spacing from global styles */ 

230 .category-buttons Button { 

231 margin: 0 1; 

232 } 

233 

234 .custom-input Button { 

235 margin: 0 1; 

236 } 

237 

238 .breadcrumbs Button { 

239 margin: 0 1; 

240 } 

241 """ 

242 

243 selected_node = reactive(None) 

244 current_ast_tree = reactive(None) 

245 navigation_stack = [] # Stack for breadcrumb navigation 

246 

247 def __init__(self, **kwargs): 

248 super().__init__( 

249 window_id="debug_ast_inspector", 

250 title="Dynamic Visual AST Inspector", 

251 mode="temporary", 

252 **kwargs 

253 ) 

254 

255 # Get inspection categories 

256 self.categories = DynamicASTAnalyzer.get_inspection_categories() 

257 

258 def on_mount(self) -> None: 

259 """Initialize the window with a default view.""" 

260 # Show available categories in the tree initially 

261 self._show_initial_state() 

262 

263 

264 

265 def compose(self) -> ComposeResult: 

266 """Compose the dynamic AST inspector.""" 

267 with Vertical(): 

268 # Top panel - Category buttons and custom input 

269 with Horizontal(classes="category-buttons"): 

270 yield Static("[bold]Inspect:[/bold]") 

271 for category_name in self.categories.keys(): 

272 yield Button(category_name, id=f"cat_{category_name.lower().replace(' ', '_')}", compact=True) 

273 

274 # Custom module input 

275 with Horizontal(classes="custom-input"): 

276 yield Label("Custom Module:") 

277 yield Input(placeholder="e.g., openhcs.core.pipeline", id="custom_module_input") 

278 yield Button("Analyze", id="analyze_custom", compact=True) 

279 yield Button("Browse File", id="browse_file", compact=True) 

280 

281 # Navigation breadcrumbs 

282 with Horizontal(classes="breadcrumbs"): 

283 yield Button("🏠 Home", id="nav_home", compact=True) 

284 yield Button("⬅️ Back", id="nav_back", compact=True, disabled=True) 

285 yield Static("", id="breadcrumb_path") 

286 

287 # Main content - AST tree and details 

288 with Horizontal(): 

289 # Left panel - AST tree 

290 with Vertical(classes="ast-tree"): 

291 yield Static("[bold]AST Structure[/bold]") 

292 tree = Tree("🔍 Click a category button or enter a module name") 

293 tree.id = "ast_tree" 

294 tree.show_root = True 

295 yield tree 

296 

297 # Right panel - node details 

298 with Vertical(classes="ast-details"): 

299 yield Static("[bold]Node Details[/bold]", id="details_title") 

300 

301 with ScrollableContainer(classes="detail-text"): 

302 yield Static("Select a node to see details", id="node_details") 

303 

304 def on_button_pressed(self, event: Button.Pressed) -> None: 

305 """Handle button presses.""" 

306 button_id = event.button.id 

307 

308 if button_id == "close": 

309 self.close_window() 

310 elif button_id.startswith("cat_"): 

311 # Category button pressed 

312 category_name = button_id[4:].replace('_', ' ').title() 

313 self._analyze_category(category_name) 

314 elif button_id == "analyze_custom": 

315 # Custom module analysis 

316 self._analyze_custom_module() 

317 elif button_id == "browse_file": 

318 # File browser (placeholder for now) 

319 self._show_file_browser() 

320 elif button_id == "nav_home": 

321 # Navigate back to home 

322 self._navigate_home() 

323 elif button_id == "nav_back": 

324 # Navigate back one level 

325 self._navigate_back() 

326 

327 def on_input_submitted(self, event: Input.Submitted) -> None: 

328 """Handle input submission.""" 

329 if event.input.id == "custom_module_input": 

330 self._analyze_custom_module() 

331 

332 def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: 

333 """Handle AST tree node selection.""" 

334 if hasattr(event.node, 'data'): 

335 if isinstance(event.node.data, ASTNodeInfo): 

336 self.selected_node = event.node.data 

337 self._update_node_details() 

338 elif isinstance(event.node.data, dict): 

339 data = event.node.data 

340 if data.get("type") == "expandable": 

341 # Expand this node 

342 self._expand_node(event.node, data["parent_node"]) 

343 elif data.get("type") == "module_placeholder": 

344 # Analyze this specific module 

345 self._analyze_single_module(data["module_name"], data["label"]) 

346 

347 def _expand_node(self, tree_node, ast_node_info: ASTNodeInfo): 

348 """Expand a node that was previously collapsed.""" 

349 # Remove the placeholder 

350 tree_node.remove() 

351 

352 # Add the actual children 

353 parent_tree_node = tree_node.parent 

354 self._populate_ast_tree(parent_tree_node, ast_node_info, max_depth=3, current_depth=0) 

355 

356 def _analyze_single_module(self, module_name: str, module_label: str): 

357 """Analyze a single module from the category view.""" 

358 # Add to navigation stack 

359 self.navigation_stack.append({ 

360 "type": "module", 

361 "module_name": module_name, 

362 "label": module_label 

363 }) 

364 self._update_navigation_ui() 

365 

366 tree = self.query_one("#ast_tree", Tree) 

367 tree.clear() 

368 tree.root.label = f"📦 {module_label} ({module_name})" 

369 

370 try: 

371 # Show loading message 

372 try: 

373 details_widget = self.query_one("#node_details", Static) 

374 details_widget.update(f"[yellow]Analyzing {module_label}...[/yellow]") 

375 except: 

376 pass 

377 

378 ast_tree = DynamicASTAnalyzer.analyze_module(module_name) 

379 if ast_tree: 

380 tree.root.data = ast_tree 

381 self._populate_ast_tree(tree.root, ast_tree, max_depth=3) # Deeper analysis for single module 

382 tree.root.expand() 

383 

384 # Update details 

385 try: 

386 details_widget = self.query_one("#node_details", Static) 

387 details_widget.update( 

388 f"[bold cyan]Module: {module_label}[/bold cyan]\n" 

389 f"[dim]({module_name})[/dim]\n\n" 

390 f"[green]✅ Analysis complete![/green]\n" 

391 f"[yellow]AST nodes:[/yellow] {len(ast_tree.children)}\n\n" 

392 "[blue]Click any node to see details[/blue]\n" 

393 "[green]Use navigation buttons to go back[/green]" 

394 ) 

395 except: 

396 pass 

397 else: 

398 error_node = tree.root.add("❌ Failed to analyze") 

399 

400 except Exception as e: 

401 error_node = tree.root.add(f"❌ Error: {str(e)[:50]}") 

402 try: 

403 details_widget = self.query_one("#node_details", Static) 

404 details_widget.update(f"[red]❌ Error analyzing {module_label}: {e}[/red]") 

405 except: 

406 pass 

407 

408 def _navigate_home(self): 

409 """Navigate back to the home screen.""" 

410 self.navigation_stack = [] 

411 self._update_navigation_ui() 

412 self._show_initial_state() 

413 

414 def _navigate_back(self): 

415 """Navigate back one level.""" 

416 if not self.navigation_stack: 

417 return 

418 

419 # Remove current level 

420 self.navigation_stack.pop() 

421 self._update_navigation_ui() 

422 

423 if not self.navigation_stack: 

424 # Back to home 

425 self._show_initial_state() 

426 else: 

427 # Recreate the previous level (but don't add to stack again) 

428 previous = self.navigation_stack[-1] 

429 if previous["type"] == "category": 

430 # Temporarily remove from stack to avoid double-adding 

431 temp_item = self.navigation_stack.pop() 

432 self._analyze_category(previous["name"]) 

433 elif previous["type"] == "module": 

434 temp_item = self.navigation_stack.pop() 

435 self._analyze_single_module(previous["module_name"], previous["label"]) 

436 elif previous["type"] == "custom_module": 

437 temp_item = self.navigation_stack.pop() 

438 # Set the input field and analyze 

439 try: 

440 input_widget = self.query_one("#custom_module_input", Input) 

441 input_widget.value = previous["module_name"] 

442 self._analyze_custom_module() 

443 except: 

444 pass 

445 

446 def _update_navigation_ui(self): 

447 """Update the navigation breadcrumbs and button states.""" 

448 try: 

449 # Update back button state 

450 back_button = self.query_one("#nav_back", Button) 

451 back_button.disabled = len(self.navigation_stack) == 0 

452 

453 # Update breadcrumb path 

454 breadcrumb = self.query_one("#breadcrumb_path", Static) 

455 if not self.navigation_stack: 

456 breadcrumb.update("🏠 Home") 

457 else: 

458 path_parts = ["🏠"] + [item["label"] for item in self.navigation_stack] 

459 breadcrumb.update(" → ".join(path_parts)) 

460 except: 

461 pass 

462 

463 def _show_initial_state(self): 

464 """Show initial state with available categories.""" 

465 # Clear navigation stack 

466 self.navigation_stack = [] 

467 self._update_navigation_ui() 

468 

469 tree = self.query_one("#ast_tree", Tree) 

470 tree.clear() 

471 tree.root.label = "🔍 Available Analysis Categories" 

472 

473 for category_name, modules in self.categories.items(): 

474 category_node = tree.root.add(f"📂 {category_name} ({len(modules)} modules)") 

475 for module_label, module_name in modules.items(): 

476 module_node = category_node.add(f"📄 {module_label}") 

477 module_node.data = {"type": "module_placeholder", "module_name": module_name, "label": module_label} 

478 

479 tree.root.expand() 

480 

481 # Update details 

482 try: 

483 details_widget = self.query_one("#node_details", Static) 

484 details_widget.update( 

485 "[bold cyan]Dynamic AST Inspector[/bold cyan]\n\n" 

486 "[yellow]Instructions:[/yellow]\n" 

487 "• Click a category button to analyze all modules in that category\n" 

488 "• Enter a custom module name and click 'Analyze'\n" 

489 "• Click any node in the AST tree to see detailed information\n" 

490 "• Use 🏠 Home or ⬅️ Back buttons to navigate\n\n" 

491 f"[green]Available Categories:[/green]\n" + 

492 "\n".join(f"{name}: {len(modules)} modules" for name, modules in self.categories.items()) 

493 ) 

494 except: 

495 pass 

496 

497 def _analyze_category(self, category_name: str): 

498 """Analyze all modules in a category.""" 

499 if category_name not in self.categories: 

500 return 

501 

502 # Add to navigation stack 

503 self.navigation_stack.append({ 

504 "type": "category", 

505 "name": category_name, 

506 "label": f"Category: {category_name}" 

507 }) 

508 self._update_navigation_ui() 

509 

510 tree = self.query_one("#ast_tree", Tree) 

511 tree.clear() 

512 tree.root.label = f"📂 {category_name} - AST Analysis" 

513 

514 modules = self.categories[category_name] 

515 for module_label, module_name in modules.items(): 

516 try: 

517 ast_tree = DynamicASTAnalyzer.analyze_module(module_name) 

518 if ast_tree: 

519 module_node = tree.root.add(f"📦 {module_label}") 

520 module_node.data = ast_tree 

521 self._populate_ast_tree(module_node, ast_tree, max_depth=1) # Shallow depth for category view 

522 else: 

523 error_node = tree.root.add(f"{module_label} (failed to analyze)") 

524 except Exception as e: 

525 error_node = tree.root.add(f"{module_label} (error: {str(e)[:50]})") 

526 

527 tree.root.expand() 

528 

529 # Update details to show category info 

530 try: 

531 details_widget = self.query_one("#node_details", Static) 

532 details_widget.update( 

533 f"[bold cyan]Category: {category_name}[/bold cyan]\n\n" 

534 f"[yellow]Modules analyzed:[/yellow] {len(modules)}\n\n" 

535 "[green]Click any module to see detailed AST[/green]\n" 

536 "[blue]Use navigation buttons to go back[/blue]" 

537 ) 

538 except: 

539 pass 

540 

541 def _analyze_custom_module(self): 

542 """Analyze a custom module entered by user.""" 

543 try: 

544 input_widget = self.query_one("#custom_module_input", Input) 

545 module_name = input_widget.value.strip() 

546 

547 if not module_name: 

548 # Show help message 

549 try: 

550 details_widget = self.query_one("#node_details", Static) 

551 details_widget.update("[yellow]Please enter a module name (e.g., openhcs.core.config)[/yellow]") 

552 except: 

553 pass 

554 return 

555 

556 # Add to navigation stack 

557 self.navigation_stack.append({ 

558 "type": "custom_module", 

559 "module_name": module_name, 

560 "label": f"Custom: {module_name}" 

561 }) 

562 self._update_navigation_ui() 

563 

564 tree = self.query_one("#ast_tree", Tree) 

565 tree.clear() 

566 tree.root.label = f"📦 Custom: {module_name}" 

567 

568 # Show loading message 

569 try: 

570 details_widget = self.query_one("#node_details", Static) 

571 details_widget.update(f"[yellow]Analyzing module: {module_name}...[/yellow]") 

572 except: 

573 pass 

574 

575 ast_tree = DynamicASTAnalyzer.analyze_module(module_name) 

576 if ast_tree: 

577 tree.root.data = ast_tree 

578 self._populate_ast_tree(tree.root, ast_tree, max_depth=3) 

579 tree.root.expand() 

580 

581 # Update details with success message 

582 try: 

583 details_widget = self.query_one("#node_details", Static) 

584 details_widget.update( 

585 f"[bold cyan]Custom Module: {module_name}[/bold cyan]\n\n" 

586 f"[green]✅ Analysis complete![/green]\n" 

587 f"[yellow]AST nodes found:[/yellow] {len(ast_tree.children)}\n\n" 

588 "[blue]Click any node in the tree to see details[/blue]\n" 

589 "[green]Use navigation buttons to go back[/green]" 

590 ) 

591 except: 

592 pass 

593 else: 

594 # Show error in tree 

595 error_node = tree.root.add("❌ Failed to analyze module") 

596 try: 

597 details_widget = self.query_one("#node_details", Static) 

598 details_widget.update(f"[red]❌ Failed to analyze module: {module_name}[/red]") 

599 except: 

600 pass 

601 

602 except Exception as e: 

603 # Show error in details 

604 try: 

605 details_widget = self.query_one("#node_details", Static) 

606 details_widget.update(f"[red]❌ Error analyzing module: {e}[/red]") 

607 except: 

608 pass 

609 

610 # Show error in tree 

611 tree = self.query_one("#ast_tree", Tree) 

612 tree.clear() 

613 tree.root.label = f"❌ Error: {module_name}" 

614 error_node = tree.root.add(f"Error: {str(e)[:100]}") 

615 

616 def _show_file_browser(self): 

617 """Show file browser (placeholder).""" 

618 try: 

619 details_widget = self.query_one("#node_details", Static) 

620 details_widget.update("[yellow]File browser not implemented yet. Use custom module input instead.[/yellow]") 

621 except: 

622 pass 

623 

624 def _populate_ast_tree(self, parent_node, ast_node_info: ASTNodeInfo, max_depth: int = 2, current_depth: int = 0): 

625 """Recursively populate the AST tree with depth control.""" 

626 if current_depth >= max_depth: 

627 return 

628 

629 for child in ast_node_info.children: 

630 # Choose appropriate icon based on node type 

631 icon = self._get_node_icon(child.node_type) 

632 child_node = parent_node.add(f"{icon} {child.description}") 

633 child_node.data = child 

634 

635 # Recursively add children up to max depth 

636 if child.children and current_depth < max_depth - 1: 

637 self._populate_ast_tree(child_node, child, max_depth, current_depth + 1) 

638 elif child.children: 

639 # Add a placeholder to show there are more children 

640 placeholder = child_node.add(f"📁 ... {len(child.children)} more nodes (click to expand)") 

641 placeholder.data = {"type": "expandable", "parent_node": child} 

642 

643 def _get_node_icon(self, node_type: str) -> str: 

644 """Get appropriate icon for AST node type.""" 

645 icons = { 

646 "ClassDef": "🏛️", 

647 "FunctionDef": "⚙️", 

648 "AsyncFunctionDef": "⚡", 

649 "Import": "📥", 

650 "ImportFrom": "📦", 

651 "Assign": "📝", 

652 "AnnAssign": "🏷️", 

653 "Expr": "💭", 

654 "If": "🔀", 

655 "For": "🔄", 

656 "While": "🔁", 

657 "Try": "🛡️", 

658 "With": "🔒", 

659 "Return": "↩️", 

660 "Yield": "⤴️" 

661 } 

662 return icons.get(node_type, "🔹") 

663 

664 def _update_node_details(self): 

665 """Update the node details panel.""" 

666 if not self.selected_node: 

667 return 

668 

669 node_info = self.selected_node 

670 details = [] 

671 

672 details.append(f"[bold cyan]AST Node: {node_info.node_type}[/bold cyan]") 

673 details.append(f"[dim]Description: {node_info.description}[/dim]") 

674 details.append("") 

675 

676 # Show node details 

677 for key, value in node_info.details.items(): 

678 if value is not None: 

679 if isinstance(value, list) and value: 

680 details.append(f"[bold yellow]{key.title()}:[/bold yellow]") 

681 for item in value: 

682 details.append(f"{item}") 

683 elif not isinstance(value, list): 

684 details.append(f"[bold yellow]{key.title()}:[/bold yellow] {value}") 

685 

686 # Show children count 

687 if node_info.children: 

688 details.append("") 

689 details.append(f"[bold blue]Children:[/bold blue] {len(node_info.children)}") 

690 for child in node_info.children[:5]: 

691 details.append(f"{child.description}") 

692 if len(node_info.children) > 5: 

693 details.append(f" ... and {len(node_info.children) - 5} more") 

694 

695 # Update the details widget 

696 try: 

697 details_widget = self.query_one("#node_details", Static) 

698 details_widget.update("\n".join(details)) 

699 except Exception: 

700 pass