janito 0.6.0__py3-none-any.whl → 0.8.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/__main__.py +127 -134
- janito/agents/__init__.py +22 -16
- janito/agents/agent.py +24 -20
- janito/agents/claudeai.py +41 -55
- janito/agents/deepseekai.py +47 -0
- janito/change/applied_blocks.py +34 -0
- janito/change/applier.py +167 -0
- janito/change/edit_blocks.py +148 -0
- janito/change/finder.py +72 -0
- janito/change/request.py +144 -0
- janito/change/validator.py +87 -251
- janito/change/view/content.py +63 -0
- janito/change/{viewer → view}/diff.py +44 -43
- janito/change/view/panels.py +201 -0
- janito/change/view/sections.py +69 -0
- janito/change/view/styling.py +140 -0
- janito/change/view/summary.py +37 -0
- janito/change/{viewer → view}/themes.py +62 -55
- janito/change/view/viewer.py +59 -0
- janito/cli/__init__.py +1 -1
- janito/cli/commands.py +68 -45
- janito/cli/functions.py +66 -111
- janito/common.py +132 -53
- janito/config.py +99 -101
- janito/data/change_prompt.txt +81 -0
- janito/data/system_prompt.txt +3 -0
- janito/qa.py +56 -66
- janito/version.py +22 -22
- janito/workspace/__init__.py +8 -7
- janito/workspace/analysis.py +120 -120
- janito/workspace/models.py +97 -0
- janito/workspace/show.py +115 -0
- janito/workspace/stats.py +42 -0
- janito/workspace/workset.py +135 -0
- janito/workspace/workspace.py +335 -0
- janito-0.8.0.dist-info/METADATA +106 -0
- janito-0.8.0.dist-info/RECORD +40 -0
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
- janito/__init__.py +0 -2
- janito/agents/openai.py +0 -53
- janito/agents/test.py +0 -34
- janito/change/__init__.py +0 -32
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +0 -23
- janito/change/analysis/__main__.py +0 -7
- janito/change/analysis/analyze.py +0 -61
- janito/change/analysis/formatting.py +0 -78
- janito/change/analysis/options.py +0 -81
- janito/change/analysis/prompts.py +0 -98
- janito/change/analysis/view/__init__.py +0 -9
- janito/change/analysis/view/terminal.py +0 -171
- janito/change/applier/__init__.py +0 -5
- janito/change/applier/file.py +0 -58
- janito/change/applier/main.py +0 -156
- janito/change/applier/text.py +0 -245
- janito/change/applier/workspace_dir.py +0 -58
- janito/change/core.py +0 -131
- janito/change/history.py +0 -44
- janito/change/operations.py +0 -7
- janito/change/parser.py +0 -289
- janito/change/play.py +0 -54
- janito/change/preview.py +0 -82
- janito/change/prompts.py +0 -126
- janito/change/test.py +0 -0
- janito/change/viewer/__init__.py +0 -11
- janito/change/viewer/content.py +0 -66
- janito/change/viewer/pager.py +0 -56
- janito/change/viewer/panels.py +0 -555
- janito/change/viewer/styling.py +0 -103
- janito/clear_statement_parser/clear_statement_format.txt +0 -328
- janito/clear_statement_parser/examples.txt +0 -326
- janito/clear_statement_parser/models.py +0 -104
- janito/clear_statement_parser/parser.py +0 -496
- janito/cli/base.py +0 -30
- janito/cli/handlers/ask.py +0 -22
- janito/cli/handlers/demo.py +0 -22
- janito/cli/handlers/request.py +0 -24
- janito/cli/handlers/scan.py +0 -9
- janito/cli/history.py +0 -61
- janito/cli/registry.py +0 -26
- janito/demo/__init__.py +0 -4
- janito/demo/data.py +0 -13
- janito/demo/mock_data.py +0 -20
- janito/demo/operations.py +0 -45
- janito/demo/runner.py +0 -59
- janito/demo/scenarios.py +0 -32
- janito/prompts.py +0 -2
- janito/review.py +0 -13
- janito/search_replace/README.md +0 -146
- janito/search_replace/__init__.py +0 -6
- janito/search_replace/__main__.py +0 -21
- janito/search_replace/core.py +0 -119
- janito/search_replace/parser.py +0 -52
- janito/search_replace/play.py +0 -61
- janito/search_replace/replacer.py +0 -36
- janito/search_replace/searcher.py +0 -299
- janito/shell/__init__.py +0 -39
- janito/shell/bus.py +0 -31
- janito/shell/commands.py +0 -195
- janito/shell/handlers.py +0 -122
- janito/shell/history.py +0 -20
- janito/shell/processor.py +0 -52
- janito/tui/__init__.py +0 -21
- janito/tui/base.py +0 -22
- janito/tui/flows/__init__.py +0 -5
- janito/tui/flows/changes.py +0 -65
- janito/tui/flows/content.py +0 -128
- janito/tui/flows/selection.py +0 -117
- janito/tui/screens/__init__.py +0 -3
- janito/tui/screens/app.py +0 -1
- janito/workspace/manager.py +0 -48
- janito/workspace/scan.py +0 -232
- janito-0.6.0.dist-info/METADATA +0 -185
- janito-0.6.0.dist-info/RECORD +0 -95
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
janito/change/validator.py
CHANGED
@@ -1,251 +1,87 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
from
|
72
|
-
from
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
return True, ""
|
89
|
-
except SyntaxError as e:
|
90
|
-
# Get detailed error information
|
91
|
-
line_num = e.lineno if e.lineno is not None else 0
|
92
|
-
col_num = e.offset if e.offset is not None else 0
|
93
|
-
line = e.text or ""
|
94
|
-
|
95
|
-
# Build error message with line pointer
|
96
|
-
pointer = " " * (col_num - 1) + "^" if col_num > 0 else ""
|
97
|
-
error_msg = (
|
98
|
-
f"Syntax error at {filepath}:{line_num}:{col_num}\n"
|
99
|
-
f"{line}\n"
|
100
|
-
f"{pointer}\n"
|
101
|
-
f"Error: {str(e)}"
|
102
|
-
)
|
103
|
-
return False, error_msg
|
104
|
-
except Exception as e:
|
105
|
-
return False, f"Parsing error in {filepath}: {str(e)}"
|
106
|
-
|
107
|
-
def validate_change(change: FileChange) -> Tuple[bool, Optional[str]]:
|
108
|
-
"""Validate a single FileChange object for structural correctness.
|
109
|
-
|
110
|
-
Validates:
|
111
|
-
- Required fields (name, operation type)
|
112
|
-
- Operation-specific requirements:
|
113
|
-
- Create/Replace: content is required
|
114
|
-
- Rename: target path is required
|
115
|
-
- Modify: at least one text change required
|
116
|
-
- Text change validations:
|
117
|
-
- Append: replace_content is required
|
118
|
-
- Delete: search_content is required
|
119
|
-
- Replace: both search_content and replace_content required
|
120
|
-
- Prevents duplicate search patterns
|
121
|
-
|
122
|
-
Args:
|
123
|
-
change: FileChange object to validate
|
124
|
-
|
125
|
-
Returns:
|
126
|
-
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
127
|
-
"""
|
128
|
-
if not change.name:
|
129
|
-
return False, "File name is required"
|
130
|
-
|
131
|
-
operation = change.operation.name.title().lower()
|
132
|
-
if operation not in ['create_file', 'replace_file', 'remove_file', 'rename_file', 'modify_file', 'move_file']:
|
133
|
-
return False, f"Invalid file operation: {change.operation}"
|
134
|
-
|
135
|
-
if operation in ['rename_file', 'move_file'] and not change.target:
|
136
|
-
return False, "Target file path is required for rename/move operation"
|
137
|
-
|
138
|
-
if operation in ['create_file', 'replace_file']:
|
139
|
-
if not change.content:
|
140
|
-
return False, f"Content is required for {change.operation} operation"
|
141
|
-
|
142
|
-
if operation == 'modify_file':
|
143
|
-
if not change.text_changes:
|
144
|
-
return False, "At least one modification is required for modify operation"
|
145
|
-
|
146
|
-
# Track search texts to avoid duplicates
|
147
|
-
seen_search_texts = set()
|
148
|
-
for mod in change.text_changes:
|
149
|
-
# Validate append operations
|
150
|
-
if mod.is_append:
|
151
|
-
if not mod.replace_content:
|
152
|
-
return False, "Replace content required for append operation"
|
153
|
-
# Validate other operations
|
154
|
-
elif not mod.is_delete:
|
155
|
-
if not mod.search_content:
|
156
|
-
return False, "Search content required for non-append modification"
|
157
|
-
|
158
|
-
if mod.search_content:
|
159
|
-
seen_search_texts.add(mod.search_content)
|
160
|
-
|
161
|
-
return True, None
|
162
|
-
|
163
|
-
def validate_all_changes(changes: List[FileChange], collected_files: Set[Path]) -> Tuple[bool, Optional[str]]:
|
164
|
-
"""Validates all aspects of the requested changes.
|
165
|
-
|
166
|
-
Performs complete validation in two phases:
|
167
|
-
1. Individual change validation:
|
168
|
-
- Structure and content requirements
|
169
|
-
- Operation-specific validations
|
170
|
-
- Text modification validations
|
171
|
-
2. Filesystem state validation:
|
172
|
-
- File existence checks
|
173
|
-
- Path conflict checks
|
174
|
-
- Python module conflict checks
|
175
|
-
|
176
|
-
Args:
|
177
|
-
changes: List of changes to validate
|
178
|
-
collected_files: Set of files found during scanning
|
179
|
-
|
180
|
-
Returns:
|
181
|
-
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
182
|
-
- If valid, error_message will be None
|
183
|
-
- If invalid, error_message will describe the validation failure
|
184
|
-
"""
|
185
|
-
# First validate individual changes
|
186
|
-
for change in changes:
|
187
|
-
is_valid, error = validate_change(change)
|
188
|
-
if not is_valid:
|
189
|
-
return False, f"Invalid change for {change.name}: {error}"
|
190
|
-
|
191
|
-
# Then validate file operations against filesystem
|
192
|
-
is_valid, error = validate_file_operations(changes, collected_files)
|
193
|
-
if not is_valid:
|
194
|
-
return False, error
|
195
|
-
|
196
|
-
return True, None
|
197
|
-
|
198
|
-
def validate_file_operations(changes: List[FileChange], collected_files: Set[Path]) -> Tuple[bool, str]:
|
199
|
-
"""Validate file operations against current filesystem state.
|
200
|
-
|
201
|
-
Validates:
|
202
|
-
- File existence for operations that require it:
|
203
|
-
- Modify: file must exist in collected files
|
204
|
-
- Replace: file must exist in collected files
|
205
|
-
- Remove: file must exist in collected files
|
206
|
-
- File non-existence for operations that require it:
|
207
|
-
- Create: file must not exist (unless marked as new)
|
208
|
-
- Rename target: target must not exist
|
209
|
-
- Path conflicts:
|
210
|
-
- Parent directory exists and is a directory
|
211
|
-
- No Python module name conflicts (dir vs file)
|
212
|
-
- Text modifications:
|
213
|
-
- Search content required for non-append modifications
|
214
|
-
- Replace content required for append operations
|
215
|
-
|
216
|
-
Args:
|
217
|
-
changes: List of file changes to validate
|
218
|
-
collected_files: Set of files that were found during scanning, includes state metadata
|
219
|
-
|
220
|
-
Returns:
|
221
|
-
Tuple[bool, str]: (is_valid, error_message)
|
222
|
-
"""
|
223
|
-
for change in changes:
|
224
|
-
# For modify operations, validate text changes
|
225
|
-
if change.operation == ChangeOperation.MODIFY_FILE:
|
226
|
-
for mod in change.text_changes:
|
227
|
-
if not mod.is_delete and not mod.is_append:
|
228
|
-
if not mod.search_content:
|
229
|
-
return False, f"Search content required for modification in {change.name}"
|
230
|
-
|
231
|
-
if mod.is_append and not mod.replace_content:
|
232
|
-
return False, f"Replace content required for append operation in {change.name}"
|
233
|
-
|
234
|
-
# Get file state if available
|
235
|
-
file_state = change.name.name.split(' (')[-1].rstrip(')') if ' (' in str(change.name) else None
|
236
|
-
is_new_file = file_state == 'new' if file_state else False
|
237
|
-
|
238
|
-
# Validate file exists for operations requiring it
|
239
|
-
if change.operation in (ChangeOperation.MODIFY_FILE, ChangeOperation.REPLACE_FILE, ChangeOperation.REMOVE_FILE):
|
240
|
-
if change.name not in collected_files and not is_new_file:
|
241
|
-
return False, f"File not found in scanned files: {change.name}"
|
242
|
-
|
243
|
-
|
244
|
-
# Validate rename/move operations
|
245
|
-
if change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
|
246
|
-
if not change.source or not change.target:
|
247
|
-
return False, "Rename/move operation requires both source and target paths"
|
248
|
-
if change.source not in collected_files and not is_new_file:
|
249
|
-
return False, f"Source file not found for rename/move: {change.source}"
|
250
|
-
|
251
|
-
return True, ""
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Set
|
3
|
+
import ast
|
4
|
+
import yaml
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import os
|
8
|
+
|
9
|
+
class Validator:
|
10
|
+
def __init__(self, preview_dir: Path):
|
11
|
+
self.preview_dir = preview_dir
|
12
|
+
self.validated_files: Set[Path] = set()
|
13
|
+
|
14
|
+
def validate_python_syntax(self, filepath: Path):
|
15
|
+
"""Validate Python file syntax using ast."""
|
16
|
+
try:
|
17
|
+
with open(filepath, 'r', encoding="utf-8") as file:
|
18
|
+
content = file.read()
|
19
|
+
ast.parse(content, filename=str(filepath))
|
20
|
+
except SyntaxError as e:
|
21
|
+
raise ValueError(f"Python syntax error in {filepath}: {e}")
|
22
|
+
except Exception as e:
|
23
|
+
raise ValueError(f"Error validating {filepath}: {e}")
|
24
|
+
|
25
|
+
def validate_files(self, files: Set[Path]):
|
26
|
+
"""Validate all modified files."""
|
27
|
+
for filepath in files:
|
28
|
+
full_path = self.preview_dir / filepath
|
29
|
+
if not full_path.exists():
|
30
|
+
raise ValueError(f"File not found after changes: {filepath}")
|
31
|
+
|
32
|
+
if filepath.suffix == '.py':
|
33
|
+
self.validate_python_syntax(full_path)
|
34
|
+
|
35
|
+
self.validated_files.add(filepath)
|
36
|
+
from rich import print as rprint
|
37
|
+
rprint(f"[green]✓[/green] Validated [cyan]{filepath}[/cyan]")
|
38
|
+
|
39
|
+
def run_tests(self):
|
40
|
+
"""Run tests if configured in janito.yaml."""
|
41
|
+
config_file = self.preview_dir / 'janito.yaml'
|
42
|
+
if not config_file.exists():
|
43
|
+
print("No test configuration found")
|
44
|
+
return
|
45
|
+
|
46
|
+
try:
|
47
|
+
with open(config_file) as f:
|
48
|
+
config = yaml.safe_load(f)
|
49
|
+
|
50
|
+
test_cmd = config.get('test_cmd')
|
51
|
+
if not test_cmd:
|
52
|
+
print("No test_cmd found in configuration")
|
53
|
+
return
|
54
|
+
|
55
|
+
print(f"Running test command: {test_cmd}")
|
56
|
+
|
57
|
+
# Save current directory
|
58
|
+
original_dir = Path.cwd()
|
59
|
+
try:
|
60
|
+
# Change to preview directory
|
61
|
+
os.chdir(self.preview_dir)
|
62
|
+
# Run the test command
|
63
|
+
exit_code = os.system(test_cmd)
|
64
|
+
finally:
|
65
|
+
# Restore original directory
|
66
|
+
os.chdir(original_dir)
|
67
|
+
|
68
|
+
if exit_code != 0:
|
69
|
+
raise ValueError(f"Test command failed with exit code {exit_code}")
|
70
|
+
|
71
|
+
from rich.panel import Panel
|
72
|
+
from rich import print as rprint
|
73
|
+
|
74
|
+
# Create a summary panel
|
75
|
+
validated_files_list = "\n".join([f"[cyan]• {f}[/cyan]" for f in sorted(self.validated_files)])
|
76
|
+
summary = Panel(
|
77
|
+
f"[green]✓[/green] All files validated successfully:\n\n{validated_files_list}",
|
78
|
+
title="[bold green]Validation Summary[/bold green]",
|
79
|
+
border_style="green"
|
80
|
+
)
|
81
|
+
rprint("\n" + summary + "\n")
|
82
|
+
print("Tests completed successfully")
|
83
|
+
|
84
|
+
except yaml.YAMLError as e:
|
85
|
+
raise ValueError(f"Error parsing janito.yaml: {e}")
|
86
|
+
except Exception as e:
|
87
|
+
raise ValueError(f"Error running tests: {e}")
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from pathlib import Path
|
3
|
+
from rich.syntax import Syntax
|
4
|
+
|
5
|
+
|
6
|
+
# Mapping of file extensions to syntax lexer names
|
7
|
+
FILE_EXTENSION_MAP = {
|
8
|
+
'.py': 'python',
|
9
|
+
'.js': 'javascript',
|
10
|
+
'.ts': 'typescript',
|
11
|
+
'.html': 'html',
|
12
|
+
'.css': 'css',
|
13
|
+
'.json': 'json',
|
14
|
+
'.md': 'markdown',
|
15
|
+
'.yaml': 'yaml',
|
16
|
+
'.yml': 'yaml',
|
17
|
+
'.sh': 'bash',
|
18
|
+
'.bash': 'bash',
|
19
|
+
'.sql': 'sql',
|
20
|
+
'.xml': 'xml',
|
21
|
+
'.cpp': 'cpp',
|
22
|
+
'.c': 'c',
|
23
|
+
'.h': 'cpp',
|
24
|
+
'.hpp': 'cpp',
|
25
|
+
'.java': 'java',
|
26
|
+
'.go': 'go',
|
27
|
+
'.rs': 'rust',
|
28
|
+
}
|
29
|
+
|
30
|
+
def get_file_syntax(filepath: Path) -> Optional[str]:
|
31
|
+
"""Get syntax lexer name based on file extension.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
filepath: Path object containing the file path
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
String containing the syntax lexer name or None if not found
|
38
|
+
"""
|
39
|
+
return ext_map.get(filepath.suffix.lower())
|
40
|
+
|
41
|
+
def create_content_preview(filepath: Path, content: str, is_new: bool = False) -> Syntax:
|
42
|
+
"""Create a preview with syntax highlighting using consistent styling
|
43
|
+
|
44
|
+
Args:
|
45
|
+
filepath: Path to the file being previewed
|
46
|
+
content: Content to preview
|
47
|
+
is_new: Whether this is a new file preview
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Syntax highlighted content
|
51
|
+
"""
|
52
|
+
# Get file info
|
53
|
+
syntax_type = get_file_syntax(filepath)
|
54
|
+
|
55
|
+
# Create syntax highlighted content
|
56
|
+
return Syntax(
|
57
|
+
content,
|
58
|
+
syntax_type or "text",
|
59
|
+
theme="monokai",
|
60
|
+
line_numbers=True,
|
61
|
+
word_wrap=True,
|
62
|
+
tab_size=4
|
63
|
+
)
|
@@ -1,43 +1,44 @@
|
|
1
|
-
from typing import List, Tuple
|
2
|
-
from difflib import SequenceMatcher
|
3
|
-
|
4
|
-
def find_common_sections(search_lines: List[str], replace_lines: List[str]) -> Tuple[List[str], List[str], List[str], List[str], List[str]]:
|
5
|
-
"""Find common sections between search and replace content"""
|
6
|
-
# Find common lines from top
|
7
|
-
common_top = []
|
8
|
-
for s, r in zip(search_lines, replace_lines):
|
9
|
-
if s == r:
|
10
|
-
common_top.append(s)
|
11
|
-
else:
|
12
|
-
break
|
13
|
-
|
14
|
-
# Find common lines from bottom
|
15
|
-
search_remaining = search_lines[len(common_top):]
|
16
|
-
replace_remaining = replace_lines[len(common_top):]
|
17
|
-
|
18
|
-
common_bottom = []
|
19
|
-
for s, r in zip(reversed(search_remaining), reversed(replace_remaining)):
|
20
|
-
if s == r:
|
21
|
-
common_bottom.insert(0, s)
|
22
|
-
else:
|
23
|
-
break
|
24
|
-
|
25
|
-
# Get the unique middle sections
|
26
|
-
search_middle = search_remaining[:-len(common_bottom)] if common_bottom else search_remaining
|
27
|
-
replace_middle = replace_remaining[:-len(common_bottom)] if common_bottom else replace_remaining
|
28
|
-
|
29
|
-
return common_top, search_middle, replace_middle, common_bottom, search_lines
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
1
|
+
from typing import List, Tuple
|
2
|
+
from difflib import SequenceMatcher
|
3
|
+
|
4
|
+
def find_common_sections(search_lines: List[str], replace_lines: List[str]) -> Tuple[List[str], List[str], List[str], List[str], List[str]]:
|
5
|
+
"""Find common sections between search and replace content"""
|
6
|
+
# Find common lines from top
|
7
|
+
common_top = []
|
8
|
+
for s, r in zip(search_lines, replace_lines):
|
9
|
+
if s == r:
|
10
|
+
common_top.append(s)
|
11
|
+
else:
|
12
|
+
break
|
13
|
+
|
14
|
+
# Find common lines from bottom
|
15
|
+
search_remaining = search_lines[len(common_top):]
|
16
|
+
replace_remaining = replace_lines[len(common_top):]
|
17
|
+
|
18
|
+
common_bottom = []
|
19
|
+
for s, r in zip(reversed(search_remaining), reversed(replace_remaining)):
|
20
|
+
if s == r:
|
21
|
+
common_bottom.insert(0, s)
|
22
|
+
else:
|
23
|
+
break
|
24
|
+
|
25
|
+
# Get the unique middle sections
|
26
|
+
search_middle = search_remaining[:-len(common_bottom)] if common_bottom else search_remaining
|
27
|
+
replace_middle = replace_remaining[:-len(common_bottom)] if common_bottom else replace_remaining
|
28
|
+
|
29
|
+
return common_top, search_middle, replace_middle, common_bottom, search_lines
|
30
|
+
|
31
|
+
|
32
|
+
def find_similar_lines(deleted_lines: List[str], added_lines: List[str], similarity_threshold: float = 0.5) -> List[Tuple[int, int, float]]:
|
33
|
+
"""Find similar lines between deleted and added content"""
|
34
|
+
similar_pairs = []
|
35
|
+
for i, del_line in enumerate(deleted_lines):
|
36
|
+
for j, add_line in enumerate(added_lines):
|
37
|
+
similarity = get_line_similarity(del_line, add_line)
|
38
|
+
if similarity >= similarity_threshold:
|
39
|
+
similar_pairs.append((i, j, similarity))
|
40
|
+
return similar_pairs
|
41
|
+
|
42
|
+
def get_line_similarity(line1: str, line2: str) -> float:
|
43
|
+
"""Calculate similarity ratio between two lines"""
|
44
|
+
return SequenceMatcher(None, line1, line2).ratio()
|