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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {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
|
janito/change/indentation.py
DELETED
@@ -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)
|