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
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user