- Context_Engineering.md: 에이전트 컨텍스트 엔지니어링 개념 정리 문서 추가 - Context_Engineering_Research.ipynb: 연구 노트북 업데이트 - deepagents_sourcecode/: docstring과 주석을 한국어로 번역
198 lines
5.9 KiB
Python
198 lines
5.9 KiB
Python
"""unified diff를 표시하기 위한 확장(diff) 위젯입니다.
|
|
|
|
Enhanced diff widget for displaying unified diffs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from textual.containers import Vertical
|
|
from textual.widgets import Static
|
|
|
|
if TYPE_CHECKING:
|
|
from textual.app import ComposeResult
|
|
|
|
|
|
def _escape_markup(text: str) -> str:
|
|
"""Escape Rich markup characters in text.
|
|
|
|
Args:
|
|
text: Text that may contain Rich markup
|
|
|
|
Returns:
|
|
Escaped text safe for Rich rendering
|
|
"""
|
|
# Escape brackets that could be interpreted as markup
|
|
return text.replace("[", r"\[").replace("]", r"\]")
|
|
|
|
|
|
def format_diff_textual(diff: str, max_lines: int | None = 100) -> str: # noqa: PLR0912
|
|
"""Format a unified diff with line numbers and colors.
|
|
|
|
Args:
|
|
diff: Unified diff string
|
|
max_lines: Maximum number of diff lines to show (None for unlimited)
|
|
|
|
Returns:
|
|
Rich-formatted diff string with line numbers
|
|
"""
|
|
if not diff:
|
|
return "[dim]No changes detected[/dim]"
|
|
|
|
lines = diff.splitlines()
|
|
|
|
# Compute stats first
|
|
additions = sum(1 for ln in lines if ln.startswith("+") and not ln.startswith("+++"))
|
|
deletions = sum(1 for ln in lines if ln.startswith("-") and not ln.startswith("---"))
|
|
|
|
# Find max line number for width calculation
|
|
max_line = 0
|
|
for line in lines:
|
|
if m := re.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)", line):
|
|
max_line = max(max_line, int(m.group(1)), int(m.group(2)))
|
|
width = max(3, len(str(max_line + len(lines))))
|
|
|
|
formatted = []
|
|
|
|
# Add stats header
|
|
stats_parts = []
|
|
if additions:
|
|
stats_parts.append(f"[green]+{additions}[/green]")
|
|
if deletions:
|
|
stats_parts.append(f"[red]-{deletions}[/red]")
|
|
if stats_parts:
|
|
formatted.append(" ".join(stats_parts))
|
|
formatted.append("") # Blank line after stats
|
|
|
|
old_num = new_num = 0
|
|
line_count = 0
|
|
|
|
for line in lines:
|
|
if max_lines and line_count >= max_lines:
|
|
formatted.append(f"\n[dim]... ({len(lines) - line_count} more lines)[/dim]")
|
|
break
|
|
|
|
# Skip file headers (--- and +++)
|
|
if line.startswith(("---", "+++")):
|
|
continue
|
|
|
|
# Handle hunk headers - just update line numbers, don't display
|
|
if m := re.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)", line):
|
|
old_num, new_num = int(m.group(1)), int(m.group(2))
|
|
continue
|
|
|
|
# Handle diff lines - use gutter bar instead of +/- prefix
|
|
content = line[1:] if line else ""
|
|
escaped_content = _escape_markup(content)
|
|
|
|
if line.startswith("-"):
|
|
# Deletion - red gutter bar, subtle red background
|
|
formatted.append(
|
|
f"[red bold]▌[/red bold][dim]{old_num:>{width}}[/dim] "
|
|
f"[on #2d1515]{escaped_content}[/on #2d1515]"
|
|
)
|
|
old_num += 1
|
|
line_count += 1
|
|
elif line.startswith("+"):
|
|
# Addition - green gutter bar, subtle green background
|
|
formatted.append(
|
|
f"[green bold]▌[/green bold][dim]{new_num:>{width}}[/dim] "
|
|
f"[on #152d15]{escaped_content}[/on #152d15]"
|
|
)
|
|
new_num += 1
|
|
line_count += 1
|
|
elif line.startswith(" "):
|
|
# Context line - dim gutter
|
|
formatted.append(f"[dim]│{old_num:>{width}}[/dim] {escaped_content}")
|
|
old_num += 1
|
|
new_num += 1
|
|
line_count += 1
|
|
elif line.strip() == "...":
|
|
# Truncation marker
|
|
formatted.append("[dim]...[/dim]")
|
|
line_count += 1
|
|
|
|
return "\n".join(formatted)
|
|
|
|
|
|
class EnhancedDiff(Vertical):
|
|
"""Widget for displaying a unified diff with syntax highlighting."""
|
|
|
|
DEFAULT_CSS = """
|
|
EnhancedDiff {
|
|
height: auto;
|
|
padding: 1;
|
|
background: $surface-darken-1;
|
|
border: round $primary;
|
|
}
|
|
|
|
EnhancedDiff .diff-title {
|
|
color: $primary;
|
|
text-style: bold;
|
|
margin-bottom: 1;
|
|
}
|
|
|
|
EnhancedDiff .diff-content {
|
|
height: auto;
|
|
}
|
|
|
|
EnhancedDiff .diff-stats {
|
|
color: $text-muted;
|
|
margin-top: 1;
|
|
}
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
diff: str,
|
|
title: str = "Diff",
|
|
max_lines: int | None = 100,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Initialize the diff widget.
|
|
|
|
Args:
|
|
diff: Unified diff string
|
|
title: Title to display above the diff
|
|
max_lines: Maximum number of diff lines to show
|
|
**kwargs: Additional arguments passed to parent
|
|
"""
|
|
super().__init__(**kwargs)
|
|
self._diff = diff
|
|
self._title = title
|
|
self._max_lines = max_lines
|
|
self._stats = self._compute_stats()
|
|
|
|
def _compute_stats(self) -> tuple[int, int]:
|
|
"""Compute additions and deletions count.
|
|
|
|
Returns:
|
|
Tuple of (additions, deletions)
|
|
"""
|
|
additions = 0
|
|
deletions = 0
|
|
for line in self._diff.splitlines():
|
|
if line.startswith("+") and not line.startswith("+++"):
|
|
additions += 1
|
|
elif line.startswith("-") and not line.startswith("---"):
|
|
deletions += 1
|
|
return additions, deletions
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the diff widget layout."""
|
|
yield Static(f"[bold cyan]═══ {self._title} ═══[/bold cyan]", classes="diff-title")
|
|
|
|
formatted = format_diff_textual(self._diff, self._max_lines)
|
|
yield Static(formatted, classes="diff-content")
|
|
|
|
additions, deletions = self._stats
|
|
if additions or deletions:
|
|
stats_parts = []
|
|
if additions:
|
|
stats_parts.append(f"[green]+{additions}[/green]")
|
|
if deletions:
|
|
stats_parts.append(f"[red]-{deletions}[/red]")
|
|
yield Static(" ".join(stats_parts), classes="diff-stats")
|