diff --git a/src/gui_2.py b/src/gui_2.py index 3068a42..ab9417b 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -243,6 +243,7 @@ class App: self._show_ast_inspector = False self._cached_ast_nodes = [] self._cached_ast_file_path = '' + self._cached_ast_file_lines = [] self.ui_editing_slices_file = None self.context_files = [] """UI-level wrapper for approving a pending tool execution ask.""" @@ -1699,12 +1700,12 @@ class App: self._cached_ast_nodes = [] import re - pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines \d+-\d+\)') + pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') stack = [] # (indent, name) for line in outline.splitlines(): m = pattern.match(line) if m: - indent_str, kind, name = m.groups() + indent_str, kind, name, start_ln, end_ln = m.groups() indent = len(indent_str) while stack and stack[-1][0] >= indent: stack.pop() @@ -1714,41 +1715,88 @@ class App: 'indent': indent, 'kind': kind, 'name': name, - 'full_path': full_path + 'full_path': full_path, + 'start_line': int(start_ln), + 'end_line': int(end_ln) }) + try: + content = mcp_client.read_file(f_path) + self._cached_ast_file_lines = content.splitlines() + except Exception: + self._cached_ast_file_lines = ["Error loading file content."] self._cached_ast_file_path = f_path imgui.text(f"Inspecting AST: {f_path}") imgui.separator() - imgui.begin_child("ast_tree_scroll", imgui.ImVec2(800, 600), True) - if not self._cached_ast_nodes: - imgui.text("No AST nodes found or error fetching outline.") - else: - for node in self._cached_ast_nodes: - indent = node['indent'] - kind = node['kind'] - name = node['name'] - full_path = node['full_path'] - - imgui.dummy(imgui.ImVec2(indent * 10, 0)) - imgui.same_line() - imgui.text(f"[{kind}] {name}") - imgui.same_line(imgui.get_window_width() - 200) - - current_mode = f_item.ast_mask.get(full_path, 'hide') - - imgui.push_id(full_path) - if imgui.radio_button("Def", current_mode == 'def'): - f_item.ast_mask[full_path] = 'def' - imgui.same_line() - if imgui.radio_button("Sig", current_mode == 'sig'): - f_item.ast_mask[full_path] = 'sig' - imgui.same_line() - if imgui.radio_button("Hide", current_mode == 'hide'): - f_item.ast_mask[full_path] = 'hide' - imgui.pop_id() - imgui.end_child() + if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_next_column() + + # --- LEFT COLUMN (Tree) --- + imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) + if not self._cached_ast_nodes: + imgui.text("No AST nodes found or error fetching outline.") + else: + for node in self._cached_ast_nodes: + indent = node['indent'] + kind = node['kind'] + name = node['name'] + full_path = node['full_path'] + + imgui.dummy(imgui.ImVec2(indent * 10, 0)) + imgui.same_line() + imgui.text(f"[{kind}] {name}") + imgui.same_line(imgui.get_window_width() - 200) + + current_mode = f_item.ast_mask.get(full_path, 'hide') + + imgui.push_id(full_path) + if imgui.radio_button("Def", current_mode == 'def'): + f_item.ast_mask[full_path] = 'def' + imgui.same_line() + if imgui.radio_button("Sig", current_mode == 'sig'): + f_item.ast_mask[full_path] = 'sig' + imgui.same_line() + if imgui.radio_button("Hide", current_mode == 'hide'): + f_item.ast_mask[full_path] = 'hide' + imgui.pop_id() + imgui.end_child() + + imgui.table_next_column() + + # --- RIGHT COLUMN (Content) --- + imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) + if not hasattr(self, '_cached_ast_file_lines') or not self._cached_ast_file_lines: + imgui.text("No file content loaded.") + else: + draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(self._cached_ast_file_lines): + line_num = i + 1 + + # Prioritize the most specific node (deepest indent) that covers the line + deepest_node = None + for node in self._cached_ast_nodes: + if node['start_line'] <= line_num <= node['end_line']: + if deepest_node is None or node['indent'] > deepest_node['indent']: + deepest_node = node + + mode = 'hide' + if deepest_node: + mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') + + pos = imgui.get_cursor_screen_pos() + line_height = imgui.get_text_line_height() + + if mode == 'def': + # Green, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) + elif mode == 'sig': + # Blue, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + + imgui.text(f"{line_num:4} | {line_text}") + imgui.end_child() + imgui.end_table() imgui.separator() if imgui.button("Close", imgui.ImVec2(120, 0)): diff --git a/tests/test_ast_inspector_extended.py b/tests/test_ast_inspector_extended.py new file mode 100644 index 0000000..f9a6013 --- /dev/null +++ b/tests/test_ast_inspector_extended.py @@ -0,0 +1,45 @@ +import unittest.mock +from unittest.mock import MagicMock, patch +from src.gui_2 import App +from src import models + +def test_ast_inspector_line_range_parsing(): + # 1. Setup mock App instance + app = MagicMock(spec=App) + app._show_ast_inspector = True + app.ui_inspecting_ast_file = models.FileItem(path="test.py") + app._cached_ast_file_path = "" + app._cached_ast_nodes = [] + + # 2. Define mock outline string with line ranges + # Note: outline_tool uses 2 spaces for indent + mock_outline = "[Func] foo (Lines 10-20)\n [Class] Bar (Lines 30-50)" + + # 3. Patch imgui and mcp_client + with patch("src.gui_2.imgui") as mock_imgui, \ + patch("src.gui_2.mcp_client.py_get_code_outline", return_value=mock_outline): + + # begin_popup_modal needs to return (expanded, opened) + mock_imgui.begin_popup_modal.return_value = (True, True) + # begin_child returns True usually + mock_imgui.begin_child.return_value = True + # radio_button returns (changed, active) + mock_imgui.radio_button.return_value = (False, False) + + # 4. Call the method + App._render_ast_inspector_modal(app) + + # 5. Assertions + assert len(app._cached_ast_nodes) == 2 + + node1 = app._cached_ast_nodes[0] + assert node1['name'] == "foo" + assert node1['kind'] == "Func" + assert node1['start_line'] == 10 + assert node1['end_line'] == 20 + + node2 = app._cached_ast_nodes[1] + assert node2['name'] == "Bar" + assert node2['kind'] == "Class" + assert node2['start_line'] == 30 + assert node2['end_line'] == 50