janito 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +105 -17
  3. janito/agents/__init__.py +9 -9
  4. janito/agents/agent.py +10 -3
  5. janito/agents/claudeai.py +15 -34
  6. janito/agents/openai.py +5 -1
  7. janito/change/__init__.py +29 -16
  8. janito/change/__main__.py +0 -0
  9. janito/{analysis → change/analysis}/__init__.py +5 -15
  10. janito/change/analysis/__main__.py +7 -0
  11. janito/change/analysis/analyze.py +62 -0
  12. janito/change/analysis/formatting.py +78 -0
  13. janito/change/analysis/options.py +81 -0
  14. janito/{analysis → change/analysis}/prompts.py +33 -18
  15. janito/change/analysis/view/__init__.py +9 -0
  16. janito/change/analysis/view/terminal.py +181 -0
  17. janito/change/applier/__init__.py +5 -0
  18. janito/change/applier/file.py +58 -0
  19. janito/change/applier/main.py +156 -0
  20. janito/change/applier/text.py +247 -0
  21. janito/change/applier/workspace_dir.py +58 -0
  22. janito/change/core.py +124 -0
  23. janito/{changehistory.py → change/history.py} +12 -14
  24. janito/change/operations.py +7 -0
  25. janito/change/parser.py +287 -0
  26. janito/change/play.py +54 -0
  27. janito/change/preview.py +82 -0
  28. janito/change/prompts.py +121 -0
  29. janito/change/test.py +0 -0
  30. janito/change/validator.py +269 -0
  31. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  32. janito/change/viewer/content.py +66 -0
  33. janito/{changeviewer → change/viewer}/diff.py +19 -4
  34. janito/change/viewer/panels.py +533 -0
  35. janito/change/viewer/styling.py +114 -0
  36. janito/{changeviewer → change/viewer}/themes.py +3 -5
  37. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  38. janito/clear_statement_parser/examples.txt +326 -0
  39. janito/clear_statement_parser/models.py +104 -0
  40. janito/clear_statement_parser/parser.py +496 -0
  41. janito/cli/base.py +30 -0
  42. janito/cli/commands.py +75 -40
  43. janito/cli/functions.py +19 -194
  44. janito/cli/history.py +61 -0
  45. janito/common.py +65 -8
  46. janito/config.py +70 -5
  47. janito/demo/__init__.py +4 -0
  48. janito/demo/data.py +13 -0
  49. janito/demo/mock_data.py +20 -0
  50. janito/demo/operations.py +45 -0
  51. janito/demo/runner.py +59 -0
  52. janito/demo/scenarios.py +32 -0
  53. janito/prompt.py +36 -0
  54. janito/qa.py +6 -14
  55. janito/search_replace/README.md +192 -0
  56. janito/search_replace/__init__.py +7 -0
  57. janito/search_replace/__main__.py +21 -0
  58. janito/search_replace/core.py +120 -0
  59. janito/search_replace/logger.py +35 -0
  60. janito/search_replace/parser.py +52 -0
  61. janito/search_replace/play.py +61 -0
  62. janito/search_replace/replacer.py +36 -0
  63. janito/search_replace/searcher.py +411 -0
  64. janito/search_replace/strategy_result.py +10 -0
  65. janito/shell/__init__.py +38 -0
  66. janito/shell/bus.py +31 -0
  67. janito/shell/commands.py +136 -0
  68. janito/shell/history.py +20 -0
  69. janito/shell/processor.py +32 -0
  70. janito/shell/prompt.py +48 -0
  71. janito/shell/registry.py +60 -0
  72. janito/tui/__init__.py +21 -0
  73. janito/tui/base.py +22 -0
  74. janito/tui/flows/__init__.py +5 -0
  75. janito/tui/flows/changes.py +65 -0
  76. janito/tui/flows/content.py +128 -0
  77. janito/tui/flows/selection.py +117 -0
  78. janito/tui/screens/__init__.py +3 -0
  79. janito/tui/screens/app.py +1 -0
  80. janito/workspace/__init__.py +6 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/show.py +141 -0
  83. janito/workspace/stats.py +43 -0
  84. janito/workspace/types.py +98 -0
  85. janito/workspace/workset.py +108 -0
  86. janito/workspace/workspace.py +114 -0
  87. janito-0.7.0.dist-info/METADATA +167 -0
  88. janito-0.7.0.dist-info/RECORD +96 -0
  89. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
  90. janito/_contextparser.py +0 -113
  91. janito/analysis/display.py +0 -149
  92. janito/analysis/options.py +0 -112
  93. janito/change/applier.py +0 -269
  94. janito/change/content.py +0 -62
  95. janito/change/indentation.py +0 -33
  96. janito/change/position.py +0 -169
  97. janito/changeviewer/panels.py +0 -268
  98. janito/changeviewer/styling.py +0 -59
  99. janito/console/__init__.py +0 -3
  100. janito/console/commands.py +0 -112
  101. janito/console/core.py +0 -62
  102. janito/console/display.py +0 -157
  103. janito/fileparser.py +0 -334
  104. janito/prompts.py +0 -81
  105. janito/scan.py +0 -176
  106. janito/tests/test_fileparser.py +0 -26
  107. janito-0.5.0.dist-info/METADATA +0 -146
  108. janito-0.5.0.dist-info/RECORD +0 -45
  109. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  110. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
janito/change/applier.py DELETED
@@ -1,269 +0,0 @@
1
- from pathlib import Path
2
- from typing import Tuple, Optional, Set
3
- from rich.console import Console
4
- from rich import box
5
- from rich.panel import Panel
6
- from rich.prompt import Confirm
7
- from datetime import datetime
8
- import subprocess
9
- import shutil
10
- import tempfile
11
-
12
- from janito.fileparser import FileChange
13
- from janito.config import config
14
- from .position import find_text_positions, format_whitespace_debug
15
- from .indentation import adjust_indentation
16
- from typing import List
17
- from ..changeviewer import preview_all_changes
18
- from ..fileparser import validate_python_syntax
19
- from ..changehistory import get_history_file_path
20
-
21
-
22
- def run_test_command(preview_dir: Path, test_cmd: str) -> Tuple[bool, str, Optional[str]]:
23
- """Run test command in preview directory.
24
- Returns (success, output, error)"""
25
- try:
26
- result = subprocess.run(
27
- test_cmd,
28
- shell=True,
29
- cwd=preview_dir,
30
- capture_output=True,
31
- text=True,
32
- timeout=300 # 5 minute timeout
33
- )
34
- return (
35
- result.returncode == 0,
36
- result.stdout,
37
- result.stderr if result.returncode != 0 else None
38
- )
39
- except subprocess.TimeoutExpired:
40
- return False, "", "Test command timed out after 5 minutes"
41
- except Exception as e:
42
- return False, "", f"Error running test: {str(e)}"
43
-
44
- def preview_and_apply_changes(changes: List[FileChange], workdir: Path, test_cmd: str = None) -> bool:
45
- """Preview changes and apply if confirmed"""
46
- console = Console()
47
-
48
- if not changes:
49
- console.print("\n[yellow]No changes were found to apply[/yellow]")
50
- return False
51
-
52
- # Show change preview
53
- preview_all_changes(console, changes)
54
-
55
- with tempfile.TemporaryDirectory() as temp_dir:
56
- preview_dir = Path(temp_dir)
57
- console.print("\n[blue]Creating preview in temporary directory...[/blue]")
58
-
59
- # Create backup directory
60
- backup_dir = workdir / '.janito' / 'backups' / datetime.now().strftime('%Y%m%d_%H%M%S')
61
- backup_dir.parent.mkdir(parents=True, exist_ok=True)
62
-
63
- # Copy existing files to preview directory
64
- if workdir.exists():
65
- if config.verbose:
66
- console.print(f"[blue]Creating backup at:[/blue] {backup_dir}")
67
- shutil.copytree(workdir, backup_dir, ignore=shutil.ignore_patterns('.janito'))
68
- shutil.copytree(workdir, preview_dir, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.janito'))
69
-
70
- # Create restore script
71
- restore_script = workdir / '.janito' / 'restore.sh'
72
- restore_script.parent.mkdir(parents=True, exist_ok=True)
73
- script_content = f"""#!/bin/bash
74
- # Restore script generated by Janito
75
- # Restores files from backup created at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
76
-
77
- # Exit on error
78
- set -e
79
-
80
- # Check if backup directory exists
81
- if [ ! -d "{backup_dir}" ]; then
82
- echo "Error: Backup directory not found at {backup_dir}"
83
- exit 1
84
- fi
85
-
86
- # Restore files from backup
87
- echo "Restoring files from backup..."
88
- cp -r "{backup_dir}"/* "{workdir}/"
89
-
90
- echo "Files restored successfully from {backup_dir}"
91
- """
92
- restore_script.write_text(script_content)
93
- restore_script.chmod(0o755)
94
-
95
- if config.verbose:
96
- console.print(f"[blue]Created restore script at:[/blue] {restore_script}")
97
-
98
- # Track modified files and apply changes to preview directory
99
- modified_files: Set[Path] = set()
100
- any_errors = False
101
- for change in changes:
102
- if config.verbose:
103
- console.print(f"[dim]Previewing changes for {change.path}...[/dim]")
104
- success, error = apply_single_change(change.path, change, workdir, preview_dir)
105
- if success and not change.remove_file:
106
- modified_files.add(change.path)
107
- if not success:
108
- if "file already exists" in str(error):
109
- console.print(f"\n[red]Error: Cannot create {change.path}[/red]")
110
- console.print("[red]File already exists and overwriting is not allowed.[/red]")
111
- else:
112
- console.print(f"\n[red]Error previewing changes for {change.path}:[/red]")
113
- console.print(f"[red]{error}[/red]")
114
- any_errors = True
115
- continue
116
-
117
- if any_errors:
118
- console.print("\n[red]Some changes could not be previewed. Aborting.[/red]")
119
- return False
120
-
121
- # Validate Python syntax
122
- python_files = {change.path for change in changes if change.path.suffix == '.py'}
123
- for filepath in python_files:
124
- preview_path = preview_dir / filepath
125
- is_valid, error_msg = validate_python_syntax(preview_path.read_text(), preview_path)
126
- if not is_valid:
127
- console.print(f"\n[red]Python syntax validation failed for {filepath}:[/red]")
128
- console.print(f"[red]{error_msg}[/red]")
129
- return False
130
-
131
- # Run tests if specified
132
- if test_cmd:
133
- console.print(f"\n[cyan]Testing changes in preview directory:[/cyan] {test_cmd}")
134
- success, output, error = run_test_command(preview_dir, test_cmd)
135
-
136
- if output:
137
- console.print("\n[bold]Test Output:[/bold]")
138
- console.print(Panel(output, box=box.ROUNDED))
139
-
140
- if not success:
141
- console.print("\n[red bold]Tests failed in preview. Changes will not be applied.[/red bold]")
142
- if error:
143
- console.print(Panel(error, title="Error", border_style="red"))
144
- return False
145
-
146
- # Final confirmation
147
- if not Confirm.ask("\n[cyan bold]Apply previewed changes to working directory?[/cyan bold]"):
148
- console.print("\n[yellow]Changes were only previewed, not applied to working directory[/yellow]")
149
- console.print("[green]Changes are stored in the history directory and can be applied later using:[/green]")
150
- changes_file = get_history_file_path(workdir)
151
- console.print(f"[cyan] {changes_file.relative_to(workdir)}[/cyan]")
152
- return False
153
-
154
- # Apply changes - copy each modified file only once
155
- console.print("\n[blue]Applying changes to working directory...[/blue]")
156
- for file_path in modified_files:
157
- console.print(f"[dim]Applying changes to {file_path}...[/dim]")
158
- target_path = workdir / file_path
159
- preview_path = preview_dir / file_path
160
- target_path.parent.mkdir(parents=True, exist_ok=True)
161
- shutil.copy2(preview_path, target_path)
162
-
163
- # Handle file removals separately
164
- for change in changes:
165
- if change.remove_file:
166
- target_path = workdir / change.path
167
- if target_path.exists():
168
- target_path.unlink()
169
- console.print(f"[red]Removed {change.path}[/red]")
170
-
171
- console.print("\n[green]Changes successfully applied to working directory![/green]")
172
- return True
173
-
174
- def apply_single_change(filepath: Path, change: FileChange, workdir: Path, preview_dir: Path) -> Tuple[bool, Optional[str]]:
175
- """Apply a single file change"""
176
- preview_path = preview_dir / filepath
177
- preview_path.parent.mkdir(parents=True, exist_ok=True)
178
-
179
- if change.remove_file:
180
- orig_path = workdir / filepath
181
- if not orig_path.exists():
182
- return False, f"Cannot remove non-existent file {filepath}"
183
- if config.debug:
184
- console = Console()
185
- console.print(f"\n[red]Removing file {filepath}[/red]")
186
- # For preview, we don't create the file in preview dir
187
- return True, None
188
-
189
- if config.debug:
190
- console = Console()
191
- console.print(f"\n[cyan]Processing change for {filepath}[/cyan]")
192
- console.print(f"[dim]Change type: {'new file' if change.is_new_file else 'modification'}[/dim]")
193
-
194
- if change.is_new_file or change.replace_file:
195
- if change.is_new_file and filepath.exists():
196
- return False, "Cannot create file - already exists"
197
- if config.debug:
198
- action = "Creating new" if change.is_new_file else "Replacing"
199
- console.print(f"[cyan]{action} file with content:[/cyan]")
200
- console.print(Panel(change.content, title="File Content"))
201
- preview_path.write_text(change.content)
202
- return True, None
203
-
204
- orig_path = workdir / filepath
205
- if not orig_path.exists():
206
- return False, f"Cannot modify non-existent file {filepath}"
207
-
208
- content = orig_path.read_text()
209
- modified = content
210
-
211
- for search, replace, description in change.search_blocks:
212
- if config.debug:
213
- console.print(f"\n[cyan]Processing search block:[/cyan] {description or 'no description'}")
214
- console.print("[yellow]Search text:[/yellow]")
215
- console.print(Panel(format_whitespace_debug(search)))
216
- if replace is not None:
217
- console.print("[yellow]Replace with:[/yellow]")
218
- console.print(Panel(format_whitespace_debug(replace)))
219
- else:
220
- console.print("[yellow]Action:[/yellow] Delete text")
221
-
222
- positions = find_text_positions(modified, search)
223
-
224
- if config.debug:
225
- console.print(f"[cyan]Found {len(positions)} matches[/cyan]")
226
-
227
- if not positions:
228
- error_context = f" ({description})" if description else ""
229
- debug_search = format_whitespace_debug(search)
230
- debug_content = format_whitespace_debug(modified)
231
- error_msg = (
232
- f"Could not find search text in {filepath}{error_context}:\n\n"
233
- f"[yellow]Search text (with whitespace markers):[/yellow]\n"
234
- f"{debug_search}\n\n"
235
- f"[yellow]File content (with whitespace markers):[/yellow]\n"
236
- f"{debug_content}"
237
- )
238
- return False, error_msg
239
-
240
- # Apply replacements from end to start to maintain position validity
241
- for start, end in reversed(positions):
242
- if config.debug:
243
- console.print(f"\n[cyan]Replacing text at positions {start}-{end}:[/cyan]")
244
- console.print("[yellow]Original segment:[/yellow]")
245
- console.print(Panel(format_whitespace_debug(modified[start:end])))
246
- if replace is not None:
247
- console.print("[yellow]Replacing with:[/yellow]")
248
- console.print(Panel(format_whitespace_debug(replace)))
249
-
250
- # Adjust replacement text indentation
251
- original_segment = modified[start:end]
252
- adjusted_replace = adjust_indentation(original_segment, replace) if replace else ""
253
-
254
- if config.debug and replace:
255
- console.print("[yellow]Adjusted replacement:[/yellow]")
256
- console.print(Panel(format_whitespace_debug(adjusted_replace)))
257
-
258
- modified = modified[:start] + adjusted_replace + modified[end:]
259
-
260
- if modified == content:
261
- if config.debug:
262
- console.print("\n[yellow]No changes were applied to the file[/yellow]")
263
- return False, "No changes were applied"
264
-
265
- if config.debug:
266
- console.print("\n[green]Changes applied successfully[/green]")
267
-
268
- preview_path.write_text(modified)
269
- return True, None
janito/change/content.py DELETED
@@ -1,62 +0,0 @@
1
-
2
- from pathlib import Path
3
- from typing import Dict, Tuple
4
- from rich.console import Console
5
- from datetime import datetime
6
-
7
- from janito.fileparser import FileChange, parse_block_changes
8
- from janito.changehistory import save_changes_to_history, get_history_file_path
9
- from janito.changeviewer import preview_all_changes
10
- from .applier import apply_single_change
11
-
12
- def get_file_type(filepath: Path) -> str:
13
- """Determine the type of saved file based on its name"""
14
- name = filepath.name.lower()
15
- if 'changes' in name:
16
- return 'changes'
17
- elif 'selected' in name:
18
- return 'selected'
19
- elif 'analysis' in name:
20
- return 'analysis'
21
- elif 'response' in name:
22
- return 'response'
23
- return 'unknown'
24
-
25
- def process_and_save_changes(content: str, request: str, workdir: Path) -> Tuple[Dict[Path, Tuple[str, str]], Path]:
26
- """Parse changes and save to history, returns (changes_dict, history_file)"""
27
- changes = parse_block_changes(content)
28
- history_file = save_changes_to_history(content, request, workdir)
29
- return changes, history_file
30
-
31
- def format_parsed_changes(changes: Dict[Path, Tuple[str, str]]) -> str:
32
- """Format parsed changes to show only file change descriptions"""
33
- result = []
34
- for filepath, (_, description) in changes.items(): # Updated tuple unpacking
35
- result.append(f"=== {filepath} ===\n{description}\n")
36
- return "\n".join(result)
37
-
38
- def apply_content_changes(content: str, request: str, workdir: Path, test_cmd: str = None) -> Tuple[bool, Path]:
39
- """Regular flow: Parse content, save to history, and apply changes."""
40
- console = Console()
41
- changes = parse_block_changes(content)
42
-
43
- if not changes:
44
- console.print("\n[yellow]No file changes were found in the response[/yellow]")
45
- return False, None
46
-
47
- history_file = save_changes_to_history(content, request, workdir)
48
- success = preview_and_apply_changes(changes, workdir, test_cmd)
49
- return success, history_file
50
-
51
- def handle_changes_file(filepath: Path, workdir: Path, test_cmd: str = None) -> Tuple[bool, Path]:
52
- """Replay flow: Load changes from file and apply them."""
53
- content = filepath.read_text()
54
- changes = parse_block_changes(content)
55
-
56
- if not changes:
57
- console = Console()
58
- console.print("\n[yellow]No file changes were found in the file[/yellow]")
59
- return False, None
60
-
61
- success = preview_and_apply_changes(changes, workdir, test_cmd)
62
- return success, filepath
@@ -1,33 +0,0 @@
1
-
2
- def adjust_indentation(original: str, replacement: str) -> str:
3
- """Adjust replacement text indentation based on original text"""
4
- if not original or not replacement:
5
- return replacement
6
-
7
- # Get first non-empty lines to compare indentation
8
- orig_lines = original.splitlines()
9
- repl_lines = replacement.splitlines()
10
-
11
- orig_first = next((l for l in orig_lines if l.strip()), '')
12
- repl_first = next((l for l in repl_lines if l.strip()), '')
13
-
14
- # Calculate indentation difference
15
- orig_indent = len(orig_first) - len(orig_first.lstrip())
16
- repl_indent = len(repl_first) - len(repl_first.lstrip())
17
- indent_delta = orig_indent - repl_indent
18
-
19
- if indent_delta == 0:
20
- return replacement
21
-
22
- # Adjust indentation for all lines
23
- adjusted_lines = []
24
- for line in repl_lines:
25
- if not line.strip(): # Preserve empty lines
26
- adjusted_lines.append(line)
27
- continue
28
-
29
- current_indent = len(line) - len(line.lstrip())
30
- new_indent = max(0, current_indent + indent_delta)
31
- adjusted_lines.append(' ' * new_indent + line.lstrip())
32
-
33
- return '\n'.join(adjusted_lines)
janito/change/position.py DELETED
@@ -1,169 +0,0 @@
1
-
2
- from typing import List, Tuple
3
- from janito.config import config
4
- from rich.console import Console
5
-
6
- def get_line_boundaries(text: str) -> List[Tuple[int, int, int, int]]:
7
- """Return list of (content_start, content_end, full_start, full_end) for each line.
8
- content_start/end exclude leading/trailing whitespace
9
- full_start/end include the whitespace and line endings"""
10
- boundaries = []
11
- start = 0
12
- for line in text.splitlines(keepends=True):
13
- content = line.strip()
14
- if content:
15
- content_start = start + len(line) - len(line.lstrip())
16
- content_end = start + len(line.rstrip())
17
- boundaries.append((content_start, content_end, start, start + len(line)))
18
- else:
19
- # Empty or whitespace-only lines
20
- boundaries.append((start, start, start, start + len(line)))
21
- start += len(line)
22
- return boundaries
23
-
24
- def normalize_content(text: str) -> Tuple[str, List[Tuple[int, int, int, int]]]:
25
- """Normalize text for searching while preserving position mapping.
26
- Returns (normalized_text, line_boundaries)"""
27
- # Replace Windows line endings
28
- text = text.replace('\r\n', '\n')
29
- text = text.replace('\r', '\n')
30
-
31
- # Get line boundaries before normalization
32
- boundaries = get_line_boundaries(text)
33
-
34
- # Create normalized version with stripped lines
35
- normalized = '\n'.join(line.strip() for line in text.splitlines())
36
-
37
- return normalized, boundaries
38
-
39
- def find_text_positions(text: str, search: str) -> List[Tuple[int, int]]:
40
- """Find all non-overlapping positions of search text in content,
41
- comparing without leading/trailing whitespace but returning original positions."""
42
- normalized_text, text_boundaries = normalize_content(text)
43
- normalized_search, search_boundaries = normalize_content(search)
44
-
45
- positions = []
46
- start = 0
47
- while True:
48
- # Find next occurrence in normalized text
49
- pos = normalized_text.find(normalized_search, start)
50
- if pos == -1:
51
- break
52
-
53
- # Find the corresponding original text boundaries
54
- search_lines = normalized_search.count('\n') + 1
55
-
56
- # Get text line number at position
57
- line_num = normalized_text.count('\n', 0, pos)
58
-
59
- if line_num + search_lines <= len(text_boundaries):
60
- # Get original start position from first line
61
- orig_start = text_boundaries[line_num][2] # full_start
62
- # Get original end position from last line
63
- orig_end = text_boundaries[line_num + search_lines - 1][3] # full_end
64
-
65
- positions.append((orig_start, orig_end))
66
-
67
- start = pos + len(normalized_search)
68
-
69
- return positions
70
-
71
- def format_whitespace_debug(text: str) -> str:
72
- """Format text with visible whitespace markers"""
73
- return text.replace(' ', '·').replace('\t', '→').replace('\n', '↵\n')
74
-
75
- def format_context_preview(lines: List[str], max_lines: int = 5) -> str:
76
- """Format context lines for display, limiting the number of lines shown"""
77
- if not lines:
78
- return "No context lines"
79
- preview = lines[:max_lines]
80
- suffix = f"\n... and {len(lines) - max_lines} more lines" if len(lines) > max_lines else ""
81
- return "\n".join(preview) + suffix
82
-
83
- def parse_and_apply_changes_sequence(input_text: str, changes_text: str) -> str:
84
- """
85
- Parse and apply changes to text:
86
- = Find and keep line (preserving whitespace)
87
- < Remove line at current position
88
- > Add line at current position
89
- """
90
- def find_initial_start(text_lines, sequence):
91
- for i in range(len(text_lines) - len(sequence) + 1):
92
- matches = True
93
- for j, seq_line in enumerate(sequence):
94
- if text_lines[i + j] != seq_line:
95
- matches = False
96
- break
97
- if matches:
98
- return i
99
-
100
- if config.debug and i < 20: # Show first 20 attempted matches
101
- console = Console()
102
- console.print(f"\n[cyan]Checking position {i}:[/cyan]")
103
- for j, seq_line in enumerate(sequence):
104
- if i + j < len(text_lines):
105
- match_status = "=" if text_lines[i + j] == seq_line else "≠"
106
- console.print(f" {match_status} Expected: '{seq_line}'")
107
- console.print(f" Found: '{text_lines[i + j]}'")
108
- return -1
109
-
110
- input_lines = input_text.splitlines()
111
- changes = changes_text.splitlines()
112
-
113
- sequence = []
114
- # Find the context sequence in the input text
115
- for line in changes:
116
- if line[0] == '=':
117
- sequence.append(line[1:])
118
- else:
119
- break
120
-
121
- start_pos = find_initial_start(input_lines, sequence)
122
-
123
- if start_pos == -1:
124
- if config.debug:
125
- console = Console()
126
- console.print("\n[red]Failed to find context sequence match in file:[/red]")
127
- console.print("[yellow]File content:[/yellow]")
128
- for i, line in enumerate(input_lines):
129
- console.print(f" {i+1:2d} | '{line}'")
130
- return input_text
131
-
132
- if config.debug:
133
- console = Console()
134
- console.print(f"\n[green]Found context match at line {start_pos + 1}[/green]")
135
-
136
- result_lines = input_lines[:start_pos]
137
- i = start_pos
138
-
139
- for change in changes:
140
- if not change:
141
- if config.debug:
142
- console.print(f" Preserving empty line")
143
- continue
144
-
145
- prefix = change[0]
146
- content = change[1:]
147
-
148
- if prefix == '=':
149
- if config.debug:
150
- console.print(f" Keep: '{content}'")
151
- result_lines.append(content)
152
- i += 1
153
- elif prefix == '<':
154
- if config.debug:
155
- console.print(f" Delete: '{content}'")
156
- i += 1
157
- elif prefix == '>':
158
- if config.debug:
159
- console.print(f" Add: '{content}'")
160
- result_lines.append(content)
161
-
162
- result_lines.extend(input_lines[i:])
163
-
164
- if config.debug:
165
- console.print("\n[yellow]Final result:[/yellow]")
166
- for i, line in enumerate(result_lines):
167
- console.print(f" {i+1:2d} | '{line}'")
168
-
169
- return '\n'.join(result_lines)