diff --git a/src/diff_viewer.py b/src/diff_viewer.py deleted file mode 100644 index b34fd096..00000000 --- a/src/diff_viewer.py +++ /dev/null @@ -1,173 +0,0 @@ -import difflib -import shutil -import os - -from dataclasses import dataclass -from pathlib import Path -from typing import List, Dict, Optional, Tuple - -from src.result_types import ErrorInfo, ErrorKind, Result - - -@dataclass -class DiffHunk: - header: str - lines: List[str] - old_start: int - old_count: int - new_start: int - new_count: int - -@dataclass -class DiffFile: - old_path: str - new_path: str - hunks: List[DiffHunk] - -def parse_hunk_header(line: str) -> tuple[int, int, int, int]: - """ - [C: tests/test_diff_viewer.py:test_parse_hunk_header] - """ - if not line.startswith("@@"): return (-1, -1, -1, -1) - - parts = line.split() - if len(parts) < 2: return (-1, -1, -1, -1) - - old_part = parts[1][1:] - new_part = parts[2][1:] - - old_parts = old_part.split(",") - new_parts = new_part.split(",") - - old_start = int(old_parts[0]) - old_count = int(old_parts[1]) if len(old_parts) > 1 else 1 - new_start = int(new_parts[0]) - new_count = int(new_parts[1]) if len(new_parts) > 1 else 1 - - return (old_start, old_count, new_start, new_count) - -def parse_diff(diff_text: str) -> List[DiffFile]: - """ - [C: src/gui_2.py:App.request_patch_from_tier4, tests/test_diff_viewer.py:test_diff_line_classification, tests/test_diff_viewer.py:test_parse_diff_empty, tests/test_diff_viewer.py:test_parse_diff_none, tests/test_diff_viewer.py:test_parse_diff_with_context, tests/test_diff_viewer.py:test_parse_multiple_files, tests/test_diff_viewer.py:test_parse_simple_diff] - """ - if not diff_text or not diff_text.strip(): - return [] - - files: List[DiffFile] = [] - current_file: Optional[DiffFile] = None - current_hunk: Optional[DiffHunk] = None - - for line in diff_text.split("\n"): - if line.startswith("--- "): - if current_file: - if current_hunk: - current_file.hunks.append(current_hunk) - current_hunk = None - files.append(current_file) - - path = line[4:] - if path.startswith("a/"): - path = path[2:] - current_file = DiffFile(old_path=path, new_path="", hunks=[]) - - elif line.startswith("+++ ") and current_file: - path = line[4:] - if path.startswith("b/"): - path = path[2:] - current_file.new_path = path - - elif line.startswith("@@") and current_file: - if current_hunk: - current_file.hunks.append(current_hunk) - - hunk_info = parse_hunk_header(line) - if hunk_info: - old_start, old_count, new_start, new_count = hunk_info - current_hunk = DiffHunk( - header = line, - lines = [], - old_start = old_start, - old_count = old_count, - new_start = new_start, - new_count = new_count - ) - else: - current_hunk = DiffHunk( - header = line, - lines = [], - old_start = 0, - old_count = 0, - new_start = 0, - new_count = 0 - ) - - elif current_hunk is not None: - current_hunk.lines.append(line) - - elif line and not line.startswith("diff ") and not line.startswith("index "): - pass - - if current_file: - if current_hunk: - current_file.hunks.append(current_hunk) - files.append(current_file) - - return files - -def get_line_color(line: str) -> str: - """ - [C: tests/test_diff_viewer.py:test_get_line_color] - """ - if line.startswith("+"): return "green" - elif line.startswith("-"): return "red" - elif line.startswith("@@"): return "cyan" - return "" - -def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> Tuple[bool, str]: - """ - [C: src/gui_2.py:App._apply_pending_patch, tests/test_diff_viewer.py:test_apply_patch_simple, tests/test_diff_viewer.py:test_apply_patch_with_context] - """ - diff_files = parse_diff(patch_text) - if not diff_files: - return False, "No valid diff found" - - results = [] - for df in diff_files: - file_path = Path(base_dir) / df.old_path - if not file_path.exists(): - results.append(f"File not found: {file_path}") - continue - - try: - with open(file_path, "r", encoding="utf-8") as f: - original_lines = f.read().splitlines(keepends=True) - - new_lines = original_lines.copy() - offset = 0 - - for hunk in df.hunks: - hunk_old_start = hunk.old_start - 1 - hunk_old_count = hunk.old_count - - replace_start = hunk_old_start + offset - replace_count = hunk_old_count - - hunk_new_content: List[str] = [] - for line in hunk.lines: - if line.startswith("+") and not line.startswith("+++"): - hunk_new_content.append(line[1:] + "\n") - elif line.startswith(" ") or (line and not line.startswith(("-", "+", "@@"))): - hunk_new_content.append(line + "\n") - - new_lines = new_lines[:replace_start] + hunk_new_content + new_lines[replace_start + replace_count:] - offset += len(hunk_new_content) - replace_count - - with open(file_path, "w", encoding="utf-8", newline="") as f: - f.writelines(new_lines) - - results.append(f"Patched: {file_path}") - except (OSError, ValueError, IndexError) as e: - _patch_err_result = Result(data=False, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Error patching {file_path}: {e}", source="diff_viewer.apply_patch_to_file", original=e)]) - return _patch_err_result.data, _patch_err_result.errors[0].message - - return True, "\n".join(results) \ No newline at end of file diff --git a/src/gui_2.py b/src/gui_2.py index fb2d1290..a7c5b26f 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -96,7 +96,6 @@ np = _LazyModule("numpy") # was: import numpy as np filedialog = _LazyModule("tkinter", "filedialog") # was: from tkinter import filedialog Tk = _LazyModule("tkinter", "Tk") # was: from tkinter import Tk -from src.diff_viewer import apply_patch_to_file from src import ai_client from src import aggregate from src import api_hooks @@ -8115,7 +8114,6 @@ def request_patch_from_tier4_result(app: "App", error: str, file_context: str) - [C: src/gui_2.py:App.request_patch_from_tier4 (L1428 legacy wrapper)] """ - from src.diff_viewer import parse_diff try: patch_text = ai_client.run_tier4_patch_generation(error, file_context) if patch_text and "---" in patch_text and "+++" in patch_text: @@ -8508,6 +8506,149 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im ) #endregion: Shaders +#region: Diff Viewer Operations (data classes live in src/patch_modal.py alongside PendingPatch; ops that gui_2 calls live here) +import difflib as _diff_difflib +import shutil as _diff_shutil +import os as _diff_os +from pathlib import Path as _diff_Path +from typing import List as _diff_List, Optional as _diff_Optional, Tuple as _diff_Tuple +from src.patch_modal import DiffHunk, DiffFile + +def parse_hunk_header(line: str) -> tuple[int, int, int, int]: + if not line.startswith("@@"): return (-1, -1, -1, -1) + parts = line.split() + if len(parts) < 2: return (-1, -1, -1, -1) + old_part = parts[1][1:] + new_part = parts[2][1:] + old_parts = old_part.split(",") + new_parts = new_part.split(",") + old_start = int(old_parts[0]) + old_count = int(old_parts[1]) if len(old_parts) > 1 else 1 + new_start = int(new_parts[0]) + new_count = int(new_parts[1]) if len(new_parts) > 1 else 1 + return (old_start, old_count, new_start, new_count) + +def parse_diff(diff_text: str) -> _diff_List[DiffFile]: + if not diff_text or not diff_text.strip(): + return [] + files: _diff_List[DiffFile] = [] + current_file: _diff_Optional[DiffFile] = None + current_hunk: _diff_Optional[DiffHunk] = None + for line in diff_text.split("\n"): + if line.startswith("--- "): + if current_file: + if current_hunk: + current_file.hunks.append(current_hunk) + current_hunk = None + files.append(current_file) + path = line[4:] + if path.startswith("a/"): + path = path[2:] + current_file = DiffFile(old_path=path, new_path="", hunks=[]) + elif line.startswith("+++ ") and current_file: + path = line[4:] + if path.startswith("b/"): + path = path[2:] + current_file.new_path = path + elif line.startswith("@@") and current_file: + if current_hunk: + current_file.hunks.append(current_hunk) + hunk_info = parse_hunk_header(line) + if hunk_info: + old_start, old_count, new_start, new_count = hunk_info + current_hunk = DiffHunk( + header = line, + lines = [], + old_start = old_start, + old_count = old_count, + new_start = new_start, + new_count = new_count + ) + else: + current_hunk = DiffHunk( + header = line, + lines = [], + old_start = 0, + old_count = 0, + new_start = 0, + new_count = 0 + ) + elif current_hunk is not None: + current_hunk.lines.append(line) + elif line and not line.startswith("diff ") and not line.startswith("index "): + pass + if current_file: + if current_hunk: + current_file.hunks.append(current_hunk) + files.append(current_file) + return files + +def get_line_color(line: str) -> str: + if line.startswith("+"): return "green" + elif line.startswith("-"): return "red" + elif line.startswith("@@"): return "cyan" + return "" + +def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> _diff_Tuple[bool, str]: + diff_files = parse_diff(patch_text) + if not diff_files: + return False, "No valid diff found" + results = [] + for df in diff_files: + file_path = _diff_Path(base_dir) / df.old_path + if not file_path.exists(): + results.append(f"File not found: {file_path}") + continue + try: + with open(file_path, "r", encoding="utf-8") as f: + original_lines = f.read().splitlines(keepends=True) + new_lines = original_lines.copy() + offset = 0 + for hunk in df.hunks: + hunk_old_start = hunk.old_start - 1 + hunk_old_count = hunk.old_count + replace_start = hunk_old_start + offset + replace_count = hunk_old_count + hunk_new_content: _diff_List[str] = [] + for line in hunk.lines: + if line.startswith("+") and not line.startswith("+++"): + hunk_new_content.append(line[1:] + "\n") + elif line.startswith(" ") or (line and not line.startswith(("-", "+", "@@"))): + hunk_new_content.append(line + "\n") + new_lines = new_lines[:replace_start] + hunk_new_content + new_lines[replace_start + replace_count:] + offset += len(hunk_new_content) - replace_count + with open(file_path, "w", encoding="utf-8", newline="") as f: + f.writelines(new_lines) + results.append(f"Patched: {file_path}") + except (OSError, ValueError, IndexError) as e: + _patch_err_result = Result(data=False, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Error patching {file_path}: {e}", source="diff_viewer.apply_patch_to_file", original=e)]) + return _patch_err_result.data, _patch_err_result.errors[0].message + return True, "\n".join(results) +#endregion: Diff Viewer Operations +def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, color: imgui.ImVec4, shadow_size: float = 10.0, rounding: float = 0.0) -> None: + r, g, b, a = color.x, color.y, color.z, color.w + steps = int(shadow_size) + if steps <= 0: return + alpha_step = a / steps + for i in range(steps): + current_alpha = a - (i * alpha_step) + current_alpha = current_alpha * (1.0 - (i / steps)**2) + if current_alpha <= 0.01: + continue + expand = float(i) + c_min = imgui.ImVec2(p_min.x - expand, p_min.y - expand) + c_max = imgui.ImVec2(p_max.x + expand, p_max.y + expand) + u32_color = imgui.get_color_u32(imgui.ImVec4(r, g, b, current_alpha)) + draw_list.add_rect( + c_min, + c_max, + u32_color, + rounding + expand if rounding > 0 else 0.0, + flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none, + thickness=1.0 + ) +#endregion: Shaders + #region: Command Palette Modal (rendering only; registry lives in src/commands.py) from src.commands import Command as _CpCommand, fuzzy_match as _cp_fuzzy_match, _close_palette, _execute as _cp_execute diff --git a/src/patch_modal.py b/src/patch_modal.py index f995957a..e95575b4 100644 --- a/src/patch_modal.py +++ b/src/patch_modal.py @@ -2,6 +2,21 @@ from dataclasses import dataclass, field from typing import Optional, Callable, List +@dataclass +class DiffHunk: + header: str + lines: List[str] + old_start: int + old_count: int + new_start: int + new_count: int + +@dataclass +class DiffFile: + old_path: str + new_path: str + hunks: List[DiffHunk] = field(default_factory=list) + @dataclass class PendingPatch: patch_text: str = "" diff --git a/tests/test_diff_viewer.py b/tests/test_diff_viewer.py index 3be120d2..7bf9a87d 100644 --- a/tests/test_diff_viewer.py +++ b/tests/test_diff_viewer.py @@ -2,8 +2,8 @@ import pytest import tempfile import os from pathlib import Path -from src.diff_viewer import ( - parse_diff, DiffFile, DiffHunk, parse_hunk_header, +from src.gui_2 import ( + parse_diff, DiffFile, DiffHunk, parse_hunk_header, get_line_color, apply_patch_to_file )