janito 0.4.0__py3-none-any.whl → 0.6.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 +1 -1
- janito/__main__.py +102 -326
- janito/agents/__init__.py +16 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +13 -17
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/change/__init__.py +32 -0
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +23 -0
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/change/analysis/prompts.py +98 -0
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -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 +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/change/history.py +44 -0
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/change/viewer/__init__.py +11 -0
- janito/change/viewer/content.py +66 -0
- janito/change/viewer/diff.py +43 -0
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/change/viewer/themes.py +55 -0
- 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/__init__.py +2 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +45 -0
- janito/cli/functions.py +111 -0
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/cli/registry.py +26 -0
- janito/common.py +41 -10
- janito/config.py +71 -6
- 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/prompts.py +1 -65
- janito/qa.py +8 -5
- janito/review.py +13 -0
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -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 +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -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 +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito/contentchange.py +0 -84
- janito/contextparser.py +0 -113
- janito/fileparser.py +0 -125
- janito/scan.py +0 -137
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/prompts.py
CHANGED
@@ -1,66 +1,2 @@
|
|
1
|
-
import re
|
2
|
-
import uuid
|
3
|
-
from typing import List, Union
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from .analysis import parse_analysis_options, AnalysisOption
|
6
|
-
|
7
1
|
# Core system prompt focused on role and purpose
|
8
|
-
SYSTEM_PROMPT = """I am Janito, your friendly software development buddy. I help you with coding tasks while being clear and concise in my responses."""
|
9
|
-
|
10
|
-
|
11
|
-
SELECTED_OPTION_PROMPT = """
|
12
|
-
Original request: {request}
|
13
|
-
|
14
|
-
Please provide detailed implementation using the following guide:
|
15
|
-
{option_text}
|
16
|
-
|
17
|
-
Current files:
|
18
|
-
<files>
|
19
|
-
{files_content}
|
20
|
-
</files>
|
21
|
-
|
22
|
-
RULES:
|
23
|
-
- When revmoing constants, ensure they are not used elsewhere
|
24
|
-
- When adding new features to python files, add the necessary imports
|
25
|
-
- Python imports should be inserted at the top of the file
|
26
|
-
|
27
|
-
Please provide the changes in this format:
|
28
|
-
|
29
|
-
## {uuid} file <filepath> modify "short file change description" ##
|
30
|
-
## {uuid} search/replace "short change description" ##
|
31
|
-
<search_content>
|
32
|
-
## {uuid} replace with ##
|
33
|
-
<replace_content>
|
34
|
-
## {uuid} file end ##
|
35
|
-
|
36
|
-
Or to delete content:
|
37
|
-
## {uuid} file <filepath> modify ##
|
38
|
-
## {uuid} search/delete "short change description" ##
|
39
|
-
<content_to_delete>
|
40
|
-
## {uuid} file end ##
|
41
|
-
|
42
|
-
For new files:
|
43
|
-
## {uuid} file <filepath> create "short description" ##
|
44
|
-
<full_file_content>
|
45
|
-
## {uuid} file end ##
|
46
|
-
|
47
|
-
RULES:
|
48
|
-
1. search_content MUST preserve the original identation/whitespace
|
49
|
-
"""
|
50
|
-
|
51
|
-
def build_selected_option_prompt(option_text: str, request: str, files_content: str = "") -> str:
|
52
|
-
"""Build prompt for selected option details
|
53
|
-
|
54
|
-
Args:
|
55
|
-
option_text: Formatted text describing the selected option
|
56
|
-
request: The original user request
|
57
|
-
files_content: Content of relevant files
|
58
|
-
"""
|
59
|
-
short_uuid = str(uuid.uuid4())[:8]
|
60
|
-
|
61
|
-
return SELECTED_OPTION_PROMPT.format(
|
62
|
-
option_text=option_text,
|
63
|
-
request=request,
|
64
|
-
files_content=files_content,
|
65
|
-
uuid=short_uuid
|
66
|
-
)
|
2
|
+
SYSTEM_PROMPT = """I am Janito, your friendly software development buddy. I help you with coding tasks while being clear and concise in my responses."""
|
janito/qa.py
CHANGED
@@ -4,10 +4,10 @@ from rich.panel import Panel
|
|
4
4
|
from rich.syntax import Syntax
|
5
5
|
from rich.table import Table
|
6
6
|
from rich.rule import Rule
|
7
|
-
from janito.
|
7
|
+
from janito.agents import AIAgent
|
8
8
|
from janito.common import progress_send_message
|
9
|
-
from
|
10
|
-
|
9
|
+
from janito.workspace import workspace
|
10
|
+
|
11
11
|
|
12
12
|
QA_PROMPT = """Please provide a clear and concise answer to the following question about the codebase:
|
13
13
|
|
@@ -22,13 +22,16 @@ Focus on providing factual information and explanations. Do not suggest code cha
|
|
22
22
|
Format your response using markdown with appropriate headers and code blocks.
|
23
23
|
"""
|
24
24
|
|
25
|
-
def ask_question(question: str, files_content: str
|
25
|
+
def ask_question(question: str, files_content: str) -> str:
|
26
26
|
"""Process a question about the codebase and return the answer"""
|
27
|
+
# Analyze workspace content if needed
|
28
|
+
workspace.analyze()
|
29
|
+
|
27
30
|
prompt = QA_PROMPT.format(
|
28
31
|
question=question,
|
29
32
|
files_content=files_content
|
30
33
|
)
|
31
|
-
return progress_send_message(
|
34
|
+
return progress_send_message(prompt)
|
32
35
|
|
33
36
|
|
34
37
|
def display_answer(answer: str, raw: bool = False) -> None:
|
janito/review.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from janito.common import progress_send_message
|
4
|
+
from janito.agents import AIAgent
|
5
|
+
|
6
|
+
def review_text(text: str, raw: bool = False) -> None:
|
7
|
+
"""Review the provided text using Claude"""
|
8
|
+
console = Console()
|
9
|
+
response = progress_send_message(f"Please review this text and provide feedback:\n\n{text}")
|
10
|
+
if raw:
|
11
|
+
console.print(response)
|
12
|
+
else:
|
13
|
+
console.print(Markdown(response))
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# Search/Replace Module
|
2
|
+
|
3
|
+
A smart search and replace module that handles code indentation and provides debugging capabilities for failed searches.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### As a Module
|
8
|
+
|
9
|
+
```python
|
10
|
+
from janito.search_replace import SearchReplacer
|
11
|
+
|
12
|
+
# Basic search/replace
|
13
|
+
source_code = """
|
14
|
+
def hello():
|
15
|
+
print("Hello")
|
16
|
+
print("World")
|
17
|
+
"""
|
18
|
+
|
19
|
+
search = """ print("Hello")
|
20
|
+
print("World")"""
|
21
|
+
|
22
|
+
replacement = """ print("Hi")
|
23
|
+
print("Universe")"""
|
24
|
+
|
25
|
+
replacer = SearchReplacer(source_code, search, replacement)
|
26
|
+
modified = replacer.replace()
|
27
|
+
```
|
28
|
+
|
29
|
+
### Command Line Debugging
|
30
|
+
|
31
|
+
When a search fails, a debug file is automatically created in `.janito/change_history/`. You can debug these files using:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
python -m janito.search_replace <debug_file>
|
35
|
+
```
|
36
|
+
|
37
|
+
Example debug file format:
|
38
|
+
```
|
39
|
+
Test: Failed search in example.py
|
40
|
+
========================================
|
41
|
+
Original:
|
42
|
+
def hello():
|
43
|
+
print("Hello")
|
44
|
+
print("World")
|
45
|
+
========================================
|
46
|
+
Search pattern:
|
47
|
+
print("Hi")
|
48
|
+
print("World")
|
49
|
+
========================================
|
50
|
+
```
|
51
|
+
|
52
|
+
## Features
|
53
|
+
|
54
|
+
- Indentation-aware searching
|
55
|
+
- Multiple search strategies:
|
56
|
+
- ExactMatch: Matches content with exact indentation
|
57
|
+
- ExactContent: Matches content ignoring indentation
|
58
|
+
- IndentAware: Matches preserving relative indentation
|
59
|
+
- Debug mode with detailed indentation analysis
|
60
|
+
- File extension specific behavior
|
61
|
+
- Automatic debug file generation for failed searches
|
62
|
+
|
63
|
+
## Search Strategies
|
64
|
+
|
65
|
+
The module uses multiple search strategies in a fallback chain to find the best match:
|
66
|
+
|
67
|
+
### ExactMatch Strategy
|
68
|
+
- Matches content exactly, including all whitespace and indentation
|
69
|
+
- Strictest matching strategy
|
70
|
+
- Example:
|
71
|
+
```python
|
72
|
+
# Pattern:
|
73
|
+
def hello():
|
74
|
+
print("Hi")
|
75
|
+
|
76
|
+
# Will only match exact indentation:
|
77
|
+
def hello():
|
78
|
+
print("Hi")
|
79
|
+
```
|
80
|
+
|
81
|
+
### IndentAware Strategy
|
82
|
+
- Preserves relative indentation between lines
|
83
|
+
- Allows different base indentation levels
|
84
|
+
- Example:
|
85
|
+
```python
|
86
|
+
# Pattern:
|
87
|
+
print("Hello")
|
88
|
+
print("World")
|
89
|
+
|
90
|
+
# Matches with different base indentation:
|
91
|
+
def test():
|
92
|
+
print("Hello")
|
93
|
+
print("World")
|
94
|
+
|
95
|
+
def other():
|
96
|
+
print("Hello")
|
97
|
+
print("World")
|
98
|
+
```
|
99
|
+
|
100
|
+
### ExactContent Strategy
|
101
|
+
- Ignores all indentation
|
102
|
+
- Matches content after stripping whitespace
|
103
|
+
- Most flexible strategy
|
104
|
+
- Example:
|
105
|
+
```python
|
106
|
+
# Pattern:
|
107
|
+
print("Hello")
|
108
|
+
print("World")
|
109
|
+
|
110
|
+
# Matches regardless of indentation:
|
111
|
+
print("Hello")
|
112
|
+
print("World")
|
113
|
+
```
|
114
|
+
|
115
|
+
### ExactContentNoComments Strategy
|
116
|
+
- Ignores indentation, comments, and empty lines
|
117
|
+
- Most flexible strategy
|
118
|
+
- Example:
|
119
|
+
```python
|
120
|
+
# Pattern:
|
121
|
+
print("Hello") # greeting
|
122
|
+
|
123
|
+
print("World") # message
|
124
|
+
|
125
|
+
# Matches:
|
126
|
+
def test():
|
127
|
+
print("Hello") # different comment
|
128
|
+
# some comment
|
129
|
+
print("World")
|
130
|
+
```
|
131
|
+
|
132
|
+
### Strategy Selection
|
133
|
+
- Strategies are tried in order: ExactMatch → IndentAware → ExactContent → ExactContentNoComments
|
134
|
+
- File extension specific behavior:
|
135
|
+
- Python files (.py): All strategies
|
136
|
+
- Java files (.java): All strategies
|
137
|
+
- JavaScript/TypeScript (.js/.ts): All strategies
|
138
|
+
- Other files: ExactMatch, ExactContent, and ExactContentNoComments
|
139
|
+
|
140
|
+
## Debug Output
|
141
|
+
|
142
|
+
When debugging failed searches, the module provides:
|
143
|
+
- Visual whitespace markers (· for spaces, → for tabs)
|
144
|
+
- Indentation analysis
|
145
|
+
- Line-by-line matching attempts
|
146
|
+
- Strategy selection information
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Main entry point for search/replace module."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
import sys
|
5
|
+
import argparse
|
6
|
+
from .play import play_file
|
7
|
+
|
8
|
+
def main():
|
9
|
+
parser = argparse.ArgumentParser(description="Debug search/replace patterns")
|
10
|
+
parser.add_argument('file', type=Path, help='Test file to analyze')
|
11
|
+
|
12
|
+
args = parser.parse_args()
|
13
|
+
|
14
|
+
if not args.file.exists():
|
15
|
+
print(f"Error: Test file not found: {args.file}")
|
16
|
+
sys.exit(1)
|
17
|
+
|
18
|
+
play_file(args.file)
|
19
|
+
|
20
|
+
if __name__ == "__main__":
|
21
|
+
main()
|
@@ -0,0 +1,119 @@
|
|
1
|
+
from typing import Optional, List
|
2
|
+
from pathlib import Path
|
3
|
+
from .searcher import Searcher
|
4
|
+
from .replacer import Replacer
|
5
|
+
|
6
|
+
class PatternNotFoundException(Exception):
|
7
|
+
"""Raised when the search pattern is not found in the source code."""
|
8
|
+
pass
|
9
|
+
|
10
|
+
class SearchReplacer:
|
11
|
+
"""Handles indentation-aware search and replace operations on Python source code."""
|
12
|
+
|
13
|
+
def __init__(self, source_code: str, search_pattern: str, replacement: Optional[str] = None,
|
14
|
+
file_ext: Optional[str] = None, debug: bool = False):
|
15
|
+
"""Initialize with source code and patterns."""
|
16
|
+
self.source_code = source_code.rstrip()
|
17
|
+
self.search_pattern = search_pattern.rstrip()
|
18
|
+
self.replacement = replacement.rstrip() if replacement else None
|
19
|
+
self.file_ext = file_ext.lower() if file_ext else None
|
20
|
+
self.pattern_found = False
|
21
|
+
self.searcher = Searcher(debug=debug)
|
22
|
+
self.replacer = Replacer(debug=debug)
|
23
|
+
|
24
|
+
# Initialize pattern base indent
|
25
|
+
first_line, _ = self.searcher.get_first_non_empty_line(self.search_pattern)
|
26
|
+
self.pattern_base_indent = len(self.searcher.get_indentation(first_line)) if first_line else 0
|
27
|
+
|
28
|
+
def find_pattern(self) -> bool:
|
29
|
+
"""Search for pattern with indentation awareness."""
|
30
|
+
try:
|
31
|
+
# Try exact matching first
|
32
|
+
exact_matches = self.searcher.exact_match(self.source_code, self.search_pattern)
|
33
|
+
if exact_matches:
|
34
|
+
if self.searcher.debug_mode:
|
35
|
+
print("[DEBUG] Found pattern using exact match")
|
36
|
+
return True
|
37
|
+
|
38
|
+
# Fall back to flexible matching
|
39
|
+
if self.searcher.debug_mode:
|
40
|
+
print("[DEBUG] No exact match found, trying flexible matching")
|
41
|
+
search_first, _ = self.searcher.get_first_non_empty_line(self.search_pattern)
|
42
|
+
search_indent = self.searcher.get_indentation(search_first)
|
43
|
+
normalized_pattern = self.searcher.normalize_pattern(self.search_pattern, search_indent)
|
44
|
+
|
45
|
+
source_lines = self.source_code.splitlines()
|
46
|
+
matches = self._find_matches(source_lines, normalized_pattern)
|
47
|
+
|
48
|
+
return bool(self.searcher._find_best_match_position(matches, source_lines, self.pattern_base_indent))
|
49
|
+
except Exception:
|
50
|
+
return False
|
51
|
+
|
52
|
+
def replace(self) -> str:
|
53
|
+
"""Perform the search and replace operation."""
|
54
|
+
if self.replacement is None:
|
55
|
+
if not self.find_pattern():
|
56
|
+
raise PatternNotFoundException("Pattern not found")
|
57
|
+
return self.source_code
|
58
|
+
|
59
|
+
source_lines = self.source_code.splitlines()
|
60
|
+
search_first, _ = self.searcher.get_first_non_empty_line(self.search_pattern)
|
61
|
+
search_indent = self.searcher.get_indentation(search_first)
|
62
|
+
normalized_pattern = self.searcher.normalize_pattern(self.search_pattern, search_indent)
|
63
|
+
|
64
|
+
matches = self._find_matches(source_lines, normalized_pattern)
|
65
|
+
best_pos = self.searcher._find_best_match_position(matches, source_lines, self.pattern_base_indent)
|
66
|
+
|
67
|
+
if best_pos is None:
|
68
|
+
raise PatternNotFoundException("Pattern not found")
|
69
|
+
|
70
|
+
if self.searcher.debug_mode:
|
71
|
+
pattern_lines = len(normalized_pattern.splitlines())
|
72
|
+
replacement_lines = len(self.replacement.splitlines()) if self.replacement else 0
|
73
|
+
print(f"\n[DEBUG] Replacing {pattern_lines} lines with {replacement_lines} lines")
|
74
|
+
context_start = max(0, best_pos - 2)
|
75
|
+
context_end = min(len(source_lines), best_pos + len(normalized_pattern.splitlines()) + 2)
|
76
|
+
print("\n[DEBUG] Context before replacement:")
|
77
|
+
for i in range(context_start, context_end):
|
78
|
+
prefix = ">>> " if context_start <= i < best_pos + len(normalized_pattern.splitlines()) else " "
|
79
|
+
print(f"[DEBUG] {prefix}Line {i + 1}: {source_lines[i]}")
|
80
|
+
|
81
|
+
result = self._apply_replacement(source_lines, best_pos, normalized_pattern)
|
82
|
+
|
83
|
+
if self.searcher.debug_mode:
|
84
|
+
print("\n[DEBUG] Context after replacement:")
|
85
|
+
result_lines = result.splitlines()
|
86
|
+
for i in range(context_start, context_end):
|
87
|
+
prefix = ">>> " if context_start <= i < best_pos + len(self.replacement.splitlines()) else " "
|
88
|
+
print(f"[DEBUG] {prefix}Line {i + 1}: {result_lines[i]}")
|
89
|
+
|
90
|
+
return result
|
91
|
+
|
92
|
+
def _find_matches(self, source_lines, normalized_pattern):
|
93
|
+
"""Find all possible matches in source."""
|
94
|
+
pattern_lines = normalized_pattern.splitlines()
|
95
|
+
return self.searcher._find_matches(source_lines, pattern_lines, self.file_ext)
|
96
|
+
|
97
|
+
def _apply_replacement(self, source_lines, match_pos, normalized_pattern):
|
98
|
+
"""Apply replacement at the matched position."""
|
99
|
+
result_lines = []
|
100
|
+
i = 0
|
101
|
+
while i < len(source_lines):
|
102
|
+
if i == match_pos:
|
103
|
+
self.pattern_found = True
|
104
|
+
match_indent = self.searcher.get_indentation(source_lines[i])
|
105
|
+
replacement_lines = self.replacer.create_indented_replacement(
|
106
|
+
match_indent, self.search_pattern, self.replacement
|
107
|
+
)
|
108
|
+
result_lines.extend(replacement_lines)
|
109
|
+
i += len(normalized_pattern.splitlines())
|
110
|
+
else:
|
111
|
+
result_lines.append(source_lines[i])
|
112
|
+
i += 1
|
113
|
+
return '\n'.join(result_lines)
|
114
|
+
|
115
|
+
def _try_match_at_position(self, pos, source_lines, normalized_pattern):
|
116
|
+
"""Check if pattern matches at given position."""
|
117
|
+
pattern_lines = normalized_pattern.splitlines()
|
118
|
+
strategies = self.searcher.get_strategies(self.file_ext)
|
119
|
+
return self.searcher.try_match_with_strategies(source_lines, pattern_lines, pos, strategies)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Dict
|
3
|
+
|
4
|
+
def parse_test_file(filepath: Path) -> List[Dict]:
|
5
|
+
"""Parse a test file containing test cases. Replacement section is optional."""
|
6
|
+
test_cases = []
|
7
|
+
current_test = {}
|
8
|
+
current_section = None
|
9
|
+
current_content = []
|
10
|
+
|
11
|
+
try:
|
12
|
+
content = filepath.read_text()
|
13
|
+
lines = content.splitlines()
|
14
|
+
|
15
|
+
for line in lines:
|
16
|
+
if line.startswith("Test: "):
|
17
|
+
if current_test:
|
18
|
+
if current_section and current_content:
|
19
|
+
current_test[current_section] = "\n".join(current_content)
|
20
|
+
test_cases.append(current_test)
|
21
|
+
current_test = {"name": line[6:].strip(), "expect_success": True}
|
22
|
+
current_section = None
|
23
|
+
current_content = []
|
24
|
+
elif line.startswith("Original:"):
|
25
|
+
if current_section and current_content:
|
26
|
+
current_test[current_section] = "\n".join(current_content)
|
27
|
+
current_section = "source"
|
28
|
+
current_content = []
|
29
|
+
elif line.startswith("Search pattern:"):
|
30
|
+
if current_section and current_content:
|
31
|
+
current_test[current_section] = "\n".join(current_content)
|
32
|
+
current_section = "search"
|
33
|
+
current_content = []
|
34
|
+
elif line.startswith("Replacement:"):
|
35
|
+
if current_section and current_content:
|
36
|
+
current_test[current_section] = "\n".join(current_content)
|
37
|
+
current_section = "replacement"
|
38
|
+
current_content = []
|
39
|
+
elif not line.startswith("="): # Skip separator lines
|
40
|
+
if current_section:
|
41
|
+
current_content.append(line)
|
42
|
+
|
43
|
+
# Add last test case
|
44
|
+
if current_test:
|
45
|
+
if current_section and current_content:
|
46
|
+
current_test[current_section] = "\n".join(current_content)
|
47
|
+
test_cases.append(current_test)
|
48
|
+
|
49
|
+
return test_cases
|
50
|
+
except Exception as e:
|
51
|
+
print(f"Error parsing test file: {e}")
|
52
|
+
return []
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional
|
3
|
+
from .parser import parse_test_file
|
4
|
+
from .core import SearchReplacer
|
5
|
+
import re
|
6
|
+
|
7
|
+
def _extract_file_ext(test_info: str) -> Optional[str]:
|
8
|
+
"""Extract file extension from test description."""
|
9
|
+
# Try to find filename or extension in the test info
|
10
|
+
ext_match = re.search(r'\.([a-zA-Z0-9]+)\b', test_info)
|
11
|
+
if (ext_match):
|
12
|
+
return f".{ext_match.group(1).lower()}"
|
13
|
+
|
14
|
+
# Look for language mentions
|
15
|
+
lang_map = {
|
16
|
+
'python': '.py',
|
17
|
+
'javascript': '.js',
|
18
|
+
'typescript': '.ts',
|
19
|
+
'java': '.java'
|
20
|
+
}
|
21
|
+
|
22
|
+
for lang, ext in lang_map.items():
|
23
|
+
if lang.lower() in test_info.lower():
|
24
|
+
return ext
|
25
|
+
|
26
|
+
return None
|
27
|
+
|
28
|
+
def play_file(filepath: Path):
|
29
|
+
"""Play back a test file and show detailed debugging info."""
|
30
|
+
test_cases = parse_test_file(filepath)
|
31
|
+
|
32
|
+
for test in test_cases:
|
33
|
+
print(f"\nTest: {test['name']}")
|
34
|
+
print("=" * 50)
|
35
|
+
|
36
|
+
if 'source' not in test or 'search' not in test:
|
37
|
+
print("Invalid test case - missing source or search pattern")
|
38
|
+
continue
|
39
|
+
|
40
|
+
file_ext = _extract_file_ext(test['name'])
|
41
|
+
print(f"\nFile type: {file_ext or 'unknown'}")
|
42
|
+
|
43
|
+
replacer = SearchReplacer(
|
44
|
+
source_code=test['source'],
|
45
|
+
search_pattern=test['search'],
|
46
|
+
replacement=test.get('replacement'),
|
47
|
+
file_ext=file_ext,
|
48
|
+
debug=True
|
49
|
+
)
|
50
|
+
|
51
|
+
try:
|
52
|
+
print("\nAttempting search/replace...")
|
53
|
+
result = replacer.replace()
|
54
|
+
print("\nResult:")
|
55
|
+
print("-" * 50)
|
56
|
+
print(result)
|
57
|
+
|
58
|
+
except Exception as e:
|
59
|
+
print(f"\nError: {str(e)}")
|
60
|
+
|
61
|
+
print("\n" + "="*50)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from typing import List
|
2
|
+
from .searcher import Searcher
|
3
|
+
|
4
|
+
class Replacer:
|
5
|
+
"""Handles replacement operations with proper indentation."""
|
6
|
+
|
7
|
+
def __init__(self, debug: bool = False):
|
8
|
+
"""Initialize replacer with debug mode."""
|
9
|
+
self.searcher = Searcher(debug=debug)
|
10
|
+
self.debug_mode = debug
|
11
|
+
|
12
|
+
def create_indented_replacement(self, match_indent: str, search_pattern: str, replacement: str) -> List[str]:
|
13
|
+
"""Create properly indented replacement lines."""
|
14
|
+
search_first, search_start_idx = self.searcher.get_first_non_empty_line(search_pattern)
|
15
|
+
replace_first, replace_start_idx = self.searcher.get_first_non_empty_line(replacement)
|
16
|
+
|
17
|
+
search_indent = self.searcher.get_indentation(search_first)
|
18
|
+
replace_indent = self.searcher.get_indentation(replace_first)
|
19
|
+
|
20
|
+
replace_lines = replacement.splitlines()
|
21
|
+
indented_replacement = []
|
22
|
+
|
23
|
+
# Calculate indentation shifts
|
24
|
+
context_shift = len(match_indent) - len(search_indent)
|
25
|
+
pattern_shift = len(replace_indent) - len(search_indent)
|
26
|
+
|
27
|
+
for i, line in enumerate(replace_lines):
|
28
|
+
if i < replace_start_idx or not line.strip():
|
29
|
+
indented_replacement.append('')
|
30
|
+
else:
|
31
|
+
line_indent = self.searcher.get_indentation(line)
|
32
|
+
rel_indent = len(line_indent) - len(replace_indent)
|
33
|
+
final_indent = ' ' * (len(match_indent) + rel_indent)
|
34
|
+
indented_replacement.append(final_indent + line.lstrip())
|
35
|
+
|
36
|
+
return indented_replacement
|