|
13 | 13 |
|
14 | 14 | import logging |
15 | 15 | import re |
| 16 | +from dataclasses import dataclass |
16 | 17 | from difflib import Differ |
17 | 18 | from functools import reduce |
18 | 19 | from itertools import islice |
19 | 20 | from pathlib import Path |
20 | 21 | from typing import Callable |
| 22 | +from typing import Dict |
| 23 | +from typing import List |
| 24 | +from typing import NamedTuple |
21 | 25 |
|
22 | 26 | from rich.console import Console |
23 | 27 | from rich.logging import RichHandler |
24 | 28 | from rich.panel import Panel |
25 | 29 | from rich.progress import track |
| 30 | +from rich.rule import Rule |
26 | 31 |
|
27 | 32 | console = Console() |
28 | 33 | logging.basicConfig( |
@@ -66,103 +71,203 @@ def write_file(path: Path, content: str) -> bool: |
66 | 71 | return False |
67 | 72 |
|
68 | 73 |
|
69 | | -def preview_changes(original: str, processed: str, context_lines: int = 2) -> None: |
70 | | - """Show a preview of the changes made.""" |
71 | | - console.print("\n[yellow]Preview of changes:[/yellow]") |
| 74 | +@dataclass |
| 75 | +class DiffStats: |
| 76 | + original_lines: int |
| 77 | + processed_lines: int |
| 78 | + difference: int |
72 | 79 |
|
73 | | - # Basic statistics |
74 | | - orig_lines = original.count("\n") |
75 | | - proc_lines = processed.count("\n") |
76 | | - diff_lines = proc_lines - orig_lines |
77 | 80 |
|
78 | | - stats_panel = Panel( |
79 | | - f"Original lines: {orig_lines}\n" |
80 | | - f"Processed lines: {proc_lines}\n" |
81 | | - f"Difference: {diff_lines:+d} lines", |
82 | | - title="Statistics", |
83 | | - border_style="blue", |
| 81 | +class DiffLine(NamedTuple): |
| 82 | + orig_line_no: int |
| 83 | + proc_line_no: int |
| 84 | + change_type: str |
| 85 | + content: str |
| 86 | + |
| 87 | + |
| 88 | +def calculate_stats(original: str, processed: str) -> DiffStats: |
| 89 | + return DiffStats( |
| 90 | + original_lines=original.count("\n"), |
| 91 | + processed_lines=processed.count("\n"), |
| 92 | + difference=processed.count("\n") - original.count("\n"), |
84 | 93 | ) |
85 | | - console.print(stats_panel) |
86 | 94 |
|
87 | | - # Create diff |
88 | | - differ = Differ() |
89 | | - diff = list(differ.compare(original.splitlines(), processed.splitlines())) |
90 | 95 |
|
91 | | - # Find changed line groups with context |
| 96 | +def create_diff_lines(diff_output: List[str]) -> List[DiffLine]: |
| 97 | + """Convert raw diff output into structured DiffLine objects with line numbers.""" |
| 98 | + diff_lines = [] |
| 99 | + orig_line_no = proc_line_no = 0 |
| 100 | + |
| 101 | + for line in diff_output: |
| 102 | + if line.startswith("? "): # Skip hint lines |
| 103 | + continue |
| 104 | + |
| 105 | + change_type = line[0:2] |
| 106 | + content = line[2:] |
| 107 | + |
| 108 | + current_orig = orig_line_no if change_type in (" ", "- ") else 0 |
| 109 | + current_proc = proc_line_no if change_type in (" ", "+ ") else 0 |
| 110 | + |
| 111 | + diff_lines.append(DiffLine(current_orig, current_proc, change_type, content)) |
| 112 | + |
| 113 | + # Update line numbers |
| 114 | + if change_type == " ": |
| 115 | + orig_line_no += 1 |
| 116 | + proc_line_no += 1 |
| 117 | + elif change_type == "- ": |
| 118 | + orig_line_no += 1 |
| 119 | + elif change_type == "+ ": |
| 120 | + proc_line_no += 1 |
| 121 | + |
| 122 | + return diff_lines |
| 123 | + |
| 124 | + |
| 125 | +def group_changes( |
| 126 | + diff_lines: List[DiffLine], context_lines: int = 5 |
| 127 | +) -> List[List[DiffLine]]: |
| 128 | + """Group changes with their context lines.""" |
92 | 129 | changes = [] |
93 | 130 | current_group = [] |
94 | 131 | in_change = False |
95 | | - last_change_line = -1 |
| 132 | + last_change_idx = -1 |
96 | 133 |
|
97 | | - for i, line in enumerate(diff): |
98 | | - if line.startswith("? "): # Skip hint lines |
99 | | - continue |
| 134 | + for i, line in enumerate(diff_lines): |
| 135 | + is_change = line.change_type in ("- ", "+ ") |
100 | 136 |
|
101 | | - is_change = line.startswith(("- ", "+ ")) |
102 | 137 | if is_change: |
103 | | - if not in_change: # Start of a new change group |
104 | | - start = max(0, i - context_lines) |
105 | | - # If we're close to previous group, connect them |
106 | | - if start <= last_change_line + context_lines: |
107 | | - start = last_change_line + 1 |
| 138 | + if not in_change: |
| 139 | + # Start of a new change group |
| 140 | + start_idx = max(0, i - context_lines) |
| 141 | + |
| 142 | + # Connect nearby groups or start new group |
| 143 | + if start_idx <= last_change_idx + context_lines: |
| 144 | + start_idx = last_change_idx + 1 |
108 | 145 | else: |
109 | 146 | if current_group: |
110 | 147 | changes.append(current_group) |
111 | 148 | current_group = [] |
112 | | - # Add previous context |
113 | | - current_group.extend( |
114 | | - l for l in diff[start:i] if not l.startswith("? ") |
115 | | - ) |
| 149 | + # Add leading context |
| 150 | + current_group.extend(diff_lines[start_idx:i]) |
| 151 | + |
116 | 152 | current_group.append(line) |
117 | 153 | in_change = True |
118 | | - last_change_line = i |
119 | | - else: |
120 | | - if in_change: |
121 | | - # Add following context |
122 | | - following_context = list( |
123 | | - islice( |
124 | | - (l for l in diff[i:] if not l.startswith("? ")), context_lines |
125 | | - ) |
| 154 | + last_change_idx = i |
| 155 | + |
| 156 | + elif in_change: |
| 157 | + # Add trailing context |
| 158 | + following_context = list( |
| 159 | + islice( |
| 160 | + (l for l in diff_lines[i:] if l.change_type == " "), context_lines |
126 | 161 | ) |
127 | | - if following_context: # Only extend if we have context to add |
128 | | - current_group.extend(following_context) |
129 | | - in_change = False |
| 162 | + ) |
| 163 | + current_group.extend(following_context) |
| 164 | + in_change = False |
130 | 165 |
|
131 | 166 | if current_group: |
132 | 167 | changes.append(current_group) |
133 | 168 |
|
134 | | - # Format and display the changes |
135 | | - formatted_output = [] |
136 | | - for i, group in enumerate(changes): |
137 | | - if i > 0: |
138 | | - formatted_output.append( |
139 | | - "[bright_black]⋮ skipped unchanged content ⋮[/bright_black]" |
140 | | - ) |
| 169 | + return changes |
141 | 170 |
|
142 | | - # Track the last line to avoid duplicates |
143 | | - last_line = None |
144 | 171 |
|
145 | | - for line in group: |
146 | | - # Skip if this line is the same as the last one |
147 | | - if line == last_line: |
148 | | - continue |
| 172 | +def get_changes( |
| 173 | + original: str, processed: str |
| 174 | +) -> tuple[Dict[str, int], List[List[DiffLine]]]: |
| 175 | + """Generate diff information and statistics.""" |
| 176 | + # Get basic statistics |
| 177 | + stats = calculate_stats(original, processed) |
| 178 | + |
| 179 | + # Create and process diff |
| 180 | + differ = Differ() |
| 181 | + diff_output = list(differ.compare(original.splitlines(), processed.splitlines())) |
| 182 | + diff_lines = create_diff_lines(diff_output) |
| 183 | + grouped_changes = group_changes(diff_lines) |
| 184 | + |
| 185 | + return vars(stats), grouped_changes |
| 186 | + |
| 187 | + |
| 188 | +@dataclass |
| 189 | +class ChangeGroup: |
| 190 | + orig_no: int |
| 191 | + proc_no: int |
| 192 | + change_type: str |
| 193 | + content: str |
| 194 | + |
| 195 | + def format_line_info(self) -> str: |
| 196 | + """Format the line numbers and separator based on change type.""" |
| 197 | + if self.change_type == " ": |
| 198 | + return f"[bright_black]{self.orig_no:4d}│{self.proc_no:4d}│[/bright_black]" |
| 199 | + elif self.change_type == "- ": |
| 200 | + return f"[bright_black]{self.orig_no:4d}│ │[/bright_black]" |
| 201 | + else: # "+" case |
| 202 | + return f"[bright_black] │{self.proc_no:4d}│[/bright_black]" |
| 203 | + |
| 204 | + def format_content(self) -> str: |
| 205 | + """Format the content based on change type.""" |
| 206 | + if self.change_type == " ": |
| 207 | + return f"[white]{self.content}[/white]" |
| 208 | + elif self.change_type == "- ": |
| 209 | + return f"[red]- {self.content}[/red]" |
| 210 | + else: # "+" case |
| 211 | + return f"[green]+ {self.content}[/green]" |
| 212 | + |
| 213 | + |
| 214 | +def create_stats_panel(stats: dict) -> Panel: |
| 215 | + """Create a formatted statistics panel.""" |
| 216 | + stats_content = ( |
| 217 | + f"Original lines: {stats['original_lines']}\n" |
| 218 | + f"Processed lines: {stats['processed_lines']}\n" |
| 219 | + f"Difference: {stats['difference']:+d} lines" |
| 220 | + ) |
| 221 | + return Panel( |
| 222 | + stats_content, |
| 223 | + title="Statistics", |
| 224 | + border_style="blue", |
| 225 | + ) |
| 226 | + |
149 | 227 |
|
150 | | - if line.startswith(" "): # unchanged |
151 | | - formatted_output.append(f"[white]{line[2:]}[/white]") |
152 | | - elif line.startswith("- "): # removed |
153 | | - formatted_output.append(f"[red]━ {line[2:]}[/red]") |
154 | | - elif line.startswith("+ "): # added |
155 | | - formatted_output.append(f"[green]+ {line[2:]}[/green]") |
| 228 | +def create_separator(prev_group: List[tuple], current_group: List[tuple]) -> Rule: |
| 229 | + """Create a separator between change groups with skip line information.""" |
| 230 | + if not prev_group: |
| 231 | + return None |
156 | 232 |
|
157 | | - last_line = line |
| 233 | + last_orig = max(l[0] for l in prev_group if l[0] > 0) |
| 234 | + next_orig = min(l[0] for l in current_group if l[0] > 0) |
| 235 | + skipped_lines = next_orig - last_orig - 1 |
158 | 236 |
|
159 | | - console.print( |
160 | | - Panel( |
161 | | - "\n".join(formatted_output), |
162 | | - title="Changes with Context", |
163 | | - border_style="yellow", |
| 237 | + if skipped_lines > 0: |
| 238 | + return Rule( |
| 239 | + f" {skipped_lines} lines skipped ", |
| 240 | + style="bright_black", |
| 241 | + characters="⋮", |
164 | 242 | ) |
165 | | - ) |
| 243 | + return Rule(style="bright_black", characters="⋮") |
| 244 | + |
| 245 | + |
| 246 | +def print_change_group(group: List[tuple]) -> None: |
| 247 | + """Print a group of changes with formatting.""" |
| 248 | + for orig_no, proc_no, change_type, content in group: |
| 249 | + change = ChangeGroup(orig_no, proc_no, change_type, content) |
| 250 | + line_info = change.format_line_info() |
| 251 | + content_formatted = change.format_content() |
| 252 | + console.print(f"{line_info} {content_formatted}") |
| 253 | + |
| 254 | + |
| 255 | +def preview_changes(original: str, processed: str) -> None: |
| 256 | + """Show a preview of the changes made.""" |
| 257 | + console.print("\n[yellow]Preview of changes:[/yellow]") |
| 258 | + |
| 259 | + # Get diff information and show statistics |
| 260 | + stats, changes = get_changes(original, processed) |
| 261 | + console.print(create_stats_panel(stats)) |
| 262 | + |
| 263 | + # Print changes with separators between groups |
| 264 | + for i, group in enumerate(changes): |
| 265 | + if i > 0: |
| 266 | + separator = create_separator(changes[i - 1], group) |
| 267 | + if separator: |
| 268 | + console.print(separator) |
| 269 | + |
| 270 | + print_change_group(group) |
166 | 271 |
|
167 | 272 |
|
168 | 273 | def process_readme( |
|
0 commit comments