Files
Ai/dctp/dctp_diff.py
T
Claude 8858a08a32 Add DCTP (Delta Code Transfer Protocol) tool for efficient AI code transfers
DCTP enables efficient transfer of AI-generated code using delta operations
instead of sending complete files for each modification. Features include:

- Parser for DCTP control commands (NEW, DELETE, INSERT_AFTER, REPLACE, RENUMBER)
- Line-numbered code with language-specific comment formats
- Backup/Undo system with session management
- Diff generation for preview functionality
- CustomTkinter GUI with project management, preview, and diff views
2025-12-25 12:59:07 +00:00

386 lines
12 KiB
Python

"""
DCTP Diff Generator - Generates diffs for preview display.
Compares old and new content and produces colored diff output
for the GUI preview.
"""
import difflib
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class DiffType(Enum):
UNCHANGED = "unchanged"
ADDED = "added"
REMOVED = "removed"
CONTEXT = "context"
@dataclass
class DiffLine:
"""Represents a single line in a diff."""
type: DiffType
line_number_old: Optional[int] # Line number in old file
line_number_new: Optional[int] # Line number in new file
content: str
@property
def prefix(self) -> str:
"""Get the diff prefix character."""
if self.type == DiffType.ADDED:
return "+"
elif self.type == DiffType.REMOVED:
return "-"
else:
return " "
def __str__(self) -> str:
old_num = str(self.line_number_old) if self.line_number_old else ""
new_num = str(self.line_number_new) if self.line_number_new else ""
return f"{old_num:>4} {new_num:>4} {self.prefix} {self.content}"
@dataclass
class DiffBlock:
"""A block of related diff lines."""
start_old: int
end_old: int
start_new: int
end_new: int
lines: list[DiffLine]
@property
def header(self) -> str:
"""Generate a unified diff style header."""
return f"@@ -{self.start_old},{self.end_old - self.start_old + 1} +{self.start_new},{self.end_new - self.start_new + 1} @@"
@dataclass
class FileDiff:
"""Complete diff for a file."""
filename: str
old_content: list[str]
new_content: list[str]
blocks: list[DiffBlock]
lines: list[DiffLine]
@property
def has_changes(self) -> bool:
return any(line.type in (DiffType.ADDED, DiffType.REMOVED) for line in self.lines)
@property
def additions(self) -> int:
return sum(1 for line in self.lines if line.type == DiffType.ADDED)
@property
def deletions(self) -> int:
return sum(1 for line in self.lines if line.type == DiffType.REMOVED)
class DiffGenerator:
"""Generates diffs between old and new content."""
def __init__(self, context_lines: int = 3):
"""
Initialize diff generator.
Args:
context_lines: Number of context lines around changes
"""
self.context_lines = context_lines
def generate(
self,
old_lines: list[str],
new_lines: list[str],
filename: str = ""
) -> FileDiff:
"""
Generate a diff between old and new content.
Args:
old_lines: Original content lines
new_lines: New content lines
filename: Optional filename for display
Returns:
FileDiff object with all diff information
"""
diff_lines: list[DiffLine] = []
# Use difflib to compute differences
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
old_line_num = 1
new_line_num = 1
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.UNCHANGED,
line_number_old=old_line_num,
line_number_new=new_line_num,
content=old_lines[i1 + idx]
))
old_line_num += 1
new_line_num += 1
elif tag == 'replace':
# Show removed lines first, then added
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.REMOVED,
line_number_old=old_line_num,
line_number_new=None,
content=old_lines[i1 + idx]
))
old_line_num += 1
for idx in range(j2 - j1):
diff_lines.append(DiffLine(
type=DiffType.ADDED,
line_number_old=None,
line_number_new=new_line_num,
content=new_lines[j1 + idx]
))
new_line_num += 1
elif tag == 'delete':
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.REMOVED,
line_number_old=old_line_num,
line_number_new=None,
content=old_lines[i1 + idx]
))
old_line_num += 1
elif tag == 'insert':
for idx in range(j2 - j1):
diff_lines.append(DiffLine(
type=DiffType.ADDED,
line_number_old=None,
line_number_new=new_line_num,
content=new_lines[j1 + idx]
))
new_line_num += 1
# Generate blocks with context
blocks = self._generate_blocks(diff_lines)
return FileDiff(
filename=filename,
old_content=old_lines,
new_content=new_lines,
blocks=blocks,
lines=diff_lines
)
def _generate_blocks(self, diff_lines: list[DiffLine]) -> list[DiffBlock]:
"""Generate diff blocks with context."""
if not diff_lines:
return []
blocks: list[DiffBlock] = []
current_block_lines: list[DiffLine] = []
in_change = False
unchanged_count = 0
for line in diff_lines:
is_change = line.type in (DiffType.ADDED, DiffType.REMOVED)
if is_change:
if not in_change:
# Starting a new change block, include context
in_change = True
unchanged_count = 0
current_block_lines.append(line)
else: # Unchanged line
if in_change:
unchanged_count += 1
if unchanged_count <= self.context_lines:
current_block_lines.append(line)
else:
# End current block and start fresh
if current_block_lines:
blocks.append(self._create_block(current_block_lines))
current_block_lines = []
in_change = False
unchanged_count = 0
else:
# Keep track of potential context lines
current_block_lines.append(line)
if len(current_block_lines) > self.context_lines:
current_block_lines.pop(0)
# Don't forget the last block
if current_block_lines and any(
l.type in (DiffType.ADDED, DiffType.REMOVED) for l in current_block_lines
):
blocks.append(self._create_block(current_block_lines))
return blocks
def _create_block(self, lines: list[DiffLine]) -> DiffBlock:
"""Create a DiffBlock from a list of lines."""
old_nums = [l.line_number_old for l in lines if l.line_number_old is not None]
new_nums = [l.line_number_new for l in lines if l.line_number_new is not None]
return DiffBlock(
start_old=min(old_nums) if old_nums else 0,
end_old=max(old_nums) if old_nums else 0,
start_new=min(new_nums) if new_nums else 0,
end_new=max(new_nums) if new_nums else 0,
lines=lines
)
def generate_unified_diff(
self,
old_lines: list[str],
new_lines: list[str],
old_filename: str = "a/file",
new_filename: str = "b/file"
) -> str:
"""
Generate a unified diff string.
Args:
old_lines: Original content lines
new_lines: New content lines
old_filename: Label for old file
new_filename: Label for new file
Returns:
Unified diff as string
"""
diff = difflib.unified_diff(
old_lines,
new_lines,
fromfile=old_filename,
tofile=new_filename,
lineterm=""
)
return "\n".join(diff)
def generate_side_by_side(
self,
old_lines: list[str],
new_lines: list[str],
width: int = 80
) -> list[tuple[str, str, str]]:
"""
Generate a side-by-side diff representation.
Args:
old_lines: Original content lines
new_lines: New content lines
width: Width for each column
Returns:
List of tuples (left_line, marker, right_line)
"""
result = []
half_width = (width - 3) // 2
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
for idx in range(i2 - i1):
line = old_lines[i1 + idx][:half_width]
result.append((line, " ", line))
elif tag == 'replace':
max_len = max(i2 - i1, j2 - j1)
for idx in range(max_len):
old_line = old_lines[i1 + idx][:half_width] if idx < i2 - i1 else ""
new_line = new_lines[j1 + idx][:half_width] if idx < j2 - j1 else ""
result.append((old_line, "|", new_line))
elif tag == 'delete':
for idx in range(i2 - i1):
old_line = old_lines[i1 + idx][:half_width]
result.append((old_line, "<", ""))
elif tag == 'insert':
for idx in range(j2 - j1):
new_line = new_lines[j1 + idx][:half_width]
result.append(("", ">", new_line))
return result
def format_diff_for_display(diff: FileDiff, use_colors: bool = True) -> str:
"""
Format a FileDiff for terminal/GUI display.
Args:
diff: The FileDiff to format
use_colors: Whether to use ANSI colors
Returns:
Formatted string
"""
lines = []
if diff.filename:
lines.append(f"--- {diff.filename}")
lines.append(f"+++ {diff.filename}")
for line in diff.lines:
if use_colors:
if line.type == DiffType.ADDED:
prefix = "\033[32m+" # Green
suffix = "\033[0m"
elif line.type == DiffType.REMOVED:
prefix = "\033[31m-" # Red
suffix = "\033[0m"
else:
prefix = " "
suffix = ""
else:
prefix = line.prefix
suffix = ""
lines.append(f"{prefix} {line.content}{suffix}")
return "\n".join(lines)
def main():
"""Test the diff generator."""
old_content = [
"def calculate_tax(amount):",
" rate = 0.19",
" if amount > 1000:",
" rate = 0.25",
" return amount * rate",
]
new_content = [
"def calculate_tax(amount):",
" rate = 0.19",
" if amount > 10000:",
" rate = 0.22",
" elif amount > 1000:",
" rate = 0.19",
" return amount * rate",
]
generator = DiffGenerator()
diff = generator.generate(old_content, new_content, "calculator.py")
print(f"File: {diff.filename}")
print(f"Additions: {diff.additions}, Deletions: {diff.deletions}")
print()
print("Diff output:")
print(format_diff_for_display(diff))
if __name__ == "__main__":
main()