feat(gui): Implement on-demand definition lookup with clickable navigation and collapsing
This commit is contained in:
@@ -46,11 +46,12 @@ def parse_symbols(text: str) -> list[str]:
|
||||
"""
|
||||
return re.findall(r"@([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)", text)
|
||||
|
||||
def get_symbol_definition(symbol: str, files: list[str]) -> tuple[str, str] | None:
|
||||
def get_symbol_definition(symbol: str, files: list[str]) -> tuple[str, str, int] | None:
|
||||
for file_path in files:
|
||||
result = mcp_client.py_get_definition(file_path, symbol)
|
||||
if 'not found' not in result.lower():
|
||||
return (file_path, result)
|
||||
result = mcp_client.py_get_symbol_info(file_path, symbol)
|
||||
if isinstance(result, tuple):
|
||||
source, line = result
|
||||
return (file_path, source, line)
|
||||
return None
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
@@ -1827,6 +1828,15 @@ class AppController:
|
||||
self.last_file_items = file_items
|
||||
self._set_status("sending...")
|
||||
user_msg = self.ui_ai_input
|
||||
|
||||
symbols = parse_symbols(user_msg)
|
||||
file_paths = [f['path'] for f in file_items]
|
||||
for symbol in symbols:
|
||||
res = get_symbol_definition(symbol, file_paths)
|
||||
if res:
|
||||
file_path, definition, line = res
|
||||
user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```'
|
||||
|
||||
base_dir = self.ui_files_base_dir
|
||||
sys.stderr.write(f"[DEBUG] _do_generate success. Prompt: {user_msg[:50]}...\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
30
src/gui_2.py
30
src/gui_2.py
@@ -21,6 +21,8 @@ from src import log_registry
|
||||
from src import log_pruner
|
||||
from src import models
|
||||
from src import app_controller
|
||||
from src import mcp_client
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed
|
||||
@@ -1404,7 +1406,33 @@ class App:
|
||||
if read_mode:
|
||||
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(entry["content"])
|
||||
content = entry["content"]
|
||||
last_idx = 0
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
for match in pattern.finditer(content):
|
||||
before = content[last_idx:match.start()]
|
||||
if before: imgui.text(before)
|
||||
header_text = match.group(0).split("\n")[0].strip()
|
||||
path = match.group(2)
|
||||
code_block = match.group(4)
|
||||
if imgui.collapsing_header(header_text):
|
||||
if imgui.button(f"[Source]##{i}_{match.start()}"):
|
||||
res = mcp_client.read_file(path)
|
||||
if res:
|
||||
self.text_viewer_title = path
|
||||
self.text_viewer_content = res
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
code_content = code_block.strip()
|
||||
if code_content.count("\n") + 1 > 50:
|
||||
imgui.begin_child(f"code_{i}_{match.start()}", imgui.ImVec2(0, 200), True)
|
||||
imgui.text(code_content)
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.text(code_content)
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after: imgui.text(after)
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
|
||||
@@ -384,6 +384,32 @@ def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
|
||||
current = found
|
||||
return current
|
||||
|
||||
def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str:
|
||||
"""
|
||||
Returns (source_code, line_number) for a specific class, function, or method definition.
|
||||
If not found, returns an error string.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file():
|
||||
return f"ERROR: not a file: {path}"
|
||||
try:
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if node:
|
||||
start = cast(int, getattr(node, "lineno"))
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return ("".join(lines[start-1:end]), start)
|
||||
return f"ERROR: definition '{name}' not found in {path}"
|
||||
except Exception as e:
|
||||
return f"ERROR retrieving definition '{name}' from '{path}': {e}"
|
||||
|
||||
def py_get_definition(path: str, name: str) -> str:
|
||||
"""
|
||||
Returns the source code for a specific class, function, or method definition.
|
||||
|
||||
Reference in New Issue
Block a user