janito 0.5.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.
Files changed (106) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +96 -15
  3. janito/agents/__init__.py +2 -8
  4. janito/agents/claudeai.py +3 -12
  5. janito/change/__init__.py +29 -16
  6. janito/change/__main__.py +0 -0
  7. janito/{analysis → change/analysis}/__init__.py +5 -15
  8. janito/change/analysis/__main__.py +7 -0
  9. janito/change/analysis/analyze.py +61 -0
  10. janito/change/analysis/formatting.py +78 -0
  11. janito/change/analysis/options.py +81 -0
  12. janito/{analysis → change/analysis}/prompts.py +35 -12
  13. janito/change/analysis/view/__init__.py +9 -0
  14. janito/change/analysis/view/terminal.py +171 -0
  15. janito/change/applier/__init__.py +5 -0
  16. janito/change/applier/file.py +58 -0
  17. janito/change/applier/main.py +156 -0
  18. janito/change/applier/text.py +245 -0
  19. janito/change/applier/workspace_dir.py +58 -0
  20. janito/change/core.py +131 -0
  21. janito/{changehistory.py → change/history.py} +12 -14
  22. janito/change/operations.py +7 -0
  23. janito/change/parser.py +289 -0
  24. janito/change/play.py +54 -0
  25. janito/change/preview.py +82 -0
  26. janito/change/prompts.py +126 -0
  27. janito/change/test.py +0 -0
  28. janito/change/validator.py +251 -0
  29. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  30. janito/change/viewer/content.py +66 -0
  31. janito/{changeviewer → change/viewer}/diff.py +19 -4
  32. janito/change/viewer/pager.py +56 -0
  33. janito/change/viewer/panels.py +555 -0
  34. janito/change/viewer/styling.py +103 -0
  35. janito/{changeviewer → change/viewer}/themes.py +3 -5
  36. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  37. janito/clear_statement_parser/examples.txt +326 -0
  38. janito/clear_statement_parser/models.py +104 -0
  39. janito/clear_statement_parser/parser.py +496 -0
  40. janito/cli/base.py +30 -0
  41. janito/cli/commands.py +30 -38
  42. janito/cli/functions.py +19 -194
  43. janito/cli/handlers/ask.py +22 -0
  44. janito/cli/handlers/demo.py +22 -0
  45. janito/cli/handlers/request.py +24 -0
  46. janito/cli/handlers/scan.py +9 -0
  47. janito/cli/history.py +61 -0
  48. janito/common.py +34 -3
  49. janito/config.py +71 -6
  50. janito/demo/__init__.py +4 -0
  51. janito/demo/data.py +13 -0
  52. janito/demo/mock_data.py +20 -0
  53. janito/demo/operations.py +45 -0
  54. janito/demo/runner.py +59 -0
  55. janito/demo/scenarios.py +32 -0
  56. janito/prompts.py +1 -80
  57. janito/qa.py +4 -3
  58. janito/search_replace/README.md +146 -0
  59. janito/search_replace/__init__.py +6 -0
  60. janito/search_replace/__main__.py +21 -0
  61. janito/search_replace/core.py +119 -0
  62. janito/search_replace/parser.py +52 -0
  63. janito/search_replace/play.py +61 -0
  64. janito/search_replace/replacer.py +36 -0
  65. janito/search_replace/searcher.py +299 -0
  66. janito/shell/__init__.py +39 -0
  67. janito/shell/bus.py +31 -0
  68. janito/shell/commands.py +195 -0
  69. janito/shell/handlers.py +122 -0
  70. janito/shell/history.py +20 -0
  71. janito/shell/processor.py +52 -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 +7 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/manager.py +48 -0
  83. janito/workspace/scan.py +232 -0
  84. janito-0.6.0.dist-info/METADATA +185 -0
  85. janito-0.6.0.dist-info/RECORD +95 -0
  86. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
  87. janito/_contextparser.py +0 -113
  88. janito/analysis/display.py +0 -149
  89. janito/analysis/options.py +0 -112
  90. janito/change/applier.py +0 -269
  91. janito/change/content.py +0 -62
  92. janito/change/indentation.py +0 -33
  93. janito/change/position.py +0 -169
  94. janito/changeviewer/panels.py +0 -268
  95. janito/changeviewer/styling.py +0 -59
  96. janito/console/__init__.py +0 -3
  97. janito/console/commands.py +0 -112
  98. janito/console/core.py +0 -62
  99. janito/console/display.py +0 -157
  100. janito/fileparser.py +0 -334
  101. janito/scan.py +0 -176
  102. janito/tests/test_fileparser.py +0 -26
  103. janito-0.5.0.dist-info/METADATA +0 -146
  104. janito-0.5.0.dist-info/RECORD +0 -45
  105. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
  106. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,20 @@
1
+ from typing import List
2
+ from .operations import CreateOperation, ModifyOperation, RemoveOperation, MockOperation
3
+
4
+ def get_mock_changes() -> List[MockOperation]:
5
+ """Get predefined mock changes for demo"""
6
+ return [
7
+ CreateOperation(
8
+ name="example/hello.py",
9
+ content="def greet():\n print('Hello, World!')\n"
10
+ ),
11
+ ModifyOperation(
12
+ name="example/utils.py",
13
+ content="def process():\n return 'Processed'\n",
14
+ original_content="def old_process():\n return 'Old'\n"
15
+ ),
16
+ RemoveOperation(
17
+ name="example/obsolete.py",
18
+ original_content="# Obsolete code\n"
19
+ )
20
+ ]
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional
3
+ from enum import Enum, auto
4
+ from pathlib import Path
5
+
6
+ class MockOperationType(Enum):
7
+ CREATE = auto()
8
+ MODIFY = auto()
9
+ REMOVE = auto()
10
+
11
+ @dataclass
12
+ class MockOperation:
13
+ """Base class for mock operations"""
14
+ operation_type: MockOperationType
15
+ name: str
16
+ reason: str
17
+
18
+ @dataclass
19
+ class CreateOperation(MockOperation):
20
+ """Operation for creating new files"""
21
+ content: str
22
+
23
+ def __init__(self, name: str, content: str, reason: str = "Create new file"):
24
+ super().__init__(MockOperationType.CREATE, name, reason)
25
+ self.content = content
26
+
27
+ @dataclass
28
+ class ModifyOperation(MockOperation):
29
+ """Operation for modifying existing files"""
30
+ content: str
31
+ original_content: str
32
+
33
+ def __init__(self, name: str, content: str, original_content: str, reason: str = "Modify existing file"):
34
+ super().__init__(MockOperationType.MODIFY, name, reason)
35
+ self.content = content
36
+ self.original_content = original_content
37
+
38
+ @dataclass
39
+ class RemoveOperation(MockOperation):
40
+ """Operation for removing files"""
41
+ original_content: Optional[str] = None
42
+
43
+ def __init__(self, name: str, original_content: Optional[str] = None, reason: str = "Remove file"):
44
+ super().__init__(MockOperationType.REMOVE, name, reason)
45
+ self.original_content = original_content
janito/demo/runner.py ADDED
@@ -0,0 +1,59 @@
1
+ from typing import List, Optional
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+ from .scenarios import DemoScenario
7
+ from .operations import MockOperationType
8
+ from ..change.viewer import preview_all_changes
9
+ from ..change.parser import FileChange, ChangeOperation
10
+
11
+ class DemoRunner:
12
+ def __init__(self):
13
+ self.console = Console()
14
+ self.scenarios: List[DemoScenario] = []
15
+
16
+ def add_scenario(self, scenario: DemoScenario) -> None:
17
+ """Add a demo scenario to the runner"""
18
+ self.scenarios.append(scenario)
19
+
20
+ def run_all(self) -> None:
21
+ """Run all registered demo scenarios"""
22
+ with Progress(
23
+ SpinnerColumn(),
24
+ TextColumn("[progress.description]{task.description}"),
25
+ console=self.console
26
+ ) as progress:
27
+ for scenario in self.scenarios:
28
+ task = progress.add_task(f"Running scenario: {scenario.name}")
29
+ self.preview_changes(scenario)
30
+ progress.update(task, completed=True)
31
+
32
+ def preview_changes(self, scenario: Optional[DemoScenario] = None) -> None:
33
+ """Preview changes for a scenario using change viewer"""
34
+ if scenario is None:
35
+ if not self.scenarios:
36
+ self.console.print("[yellow]No scenarios to preview[/yellow]")
37
+ return
38
+ scenario = self.scenarios[0]
39
+
40
+ # Convert mock changes to FileChange objects
41
+ changes = []
42
+ for mock in scenario.changes:
43
+ # Map mock operation type to ChangeOperation
44
+ operation_map = {
45
+ MockOperationType.CREATE: ChangeOperation.CREATE_FILE,
46
+ MockOperationType.MODIFY: ChangeOperation.MODIFY_FILE,
47
+ MockOperationType.REMOVE: ChangeOperation.REMOVE_FILE
48
+ }
49
+ operation = operation_map[mock.operation_type]
50
+ change = FileChange(
51
+ operation=operation,
52
+ name=Path(mock.name),
53
+ content=mock.content if hasattr(mock, 'content') else None,
54
+ original_content=mock.original_content if hasattr(mock, 'original_content') else None
55
+ )
56
+ changes.append(change)
57
+
58
+ # Show changes using change viewer
59
+ preview_all_changes(self.console, changes)
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Dict, Optional
3
+ from rich.text import Text
4
+ from pathlib import Path
5
+ from .operations import MockOperation
6
+ from .mock_data import get_mock_changes
7
+
8
+ @dataclass
9
+ class DemoScenario:
10
+ name: str
11
+ description: str
12
+ changes: List[MockOperation]
13
+
14
+ def get_preview(self) -> Text:
15
+ """Get a preview of the changes"""
16
+ text = Text()
17
+ text.append(f"Description: {self.description}\n\n", style="cyan")
18
+
19
+ # Group changes by operation
20
+ by_operation = {}
21
+ for change in self.changes:
22
+ if change.operation not in by_operation:
23
+ by_operation[change.operation] = []
24
+ by_operation[change.operation].append(change)
25
+
26
+ # Show changes grouped by operation
27
+ for operation_type, changes in by_operation.items():
28
+ text.append(f"\n{operation_type.name.title()} Operations:\n", style="yellow")
29
+ for change in changes:
30
+ text.append(f"• {change.name}\n", style="white")
31
+
32
+ return text
janito/prompts.py CHANGED
@@ -1,81 +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 removing 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
- - For complete file replacements, only use for existing files marked as modified
27
- - File replacements must preserve the essential functionality
28
- - When multiple changes affect the same code block, combine them into a single change
29
- - if no changes are required answer only the reason in the format: <no_changes_required>reason for no changes<no_changes_required>
30
-
31
- Please provide the changes in this format:
32
-
33
- For incremental changes:
34
- ## {uuid} file <filepath> modify "short file change description" ##
35
- ## {uuid} search/replace "short change description" ##
36
- <search_content>
37
- ## {uuid} replace with ##
38
- <replace_content>
39
- ## {uuid} file end ##
40
-
41
- For complete file replacement (only for existing modified files):
42
- ## {uuid} file <filepath> replace "short file description" ##
43
- <full_file_content>
44
- ## {uuid} file end ##
45
-
46
- For new files:
47
- ## {uuid} file <filepath> create "short file description" ##
48
- <full_file_content>
49
- ## {uuid} file end ##
50
-
51
- For content deletion:
52
- ## {uuid} file <filepath> modify ##
53
- ## {uuid} search/delete "short change description" ##
54
- <content_to_delete>
55
- ## {uuid} file end ##
56
-
57
- For file removal:
58
- ## {uuid} file <filepath> remove "short removal reason" ##
59
- ## {uuid} file end ##
60
-
61
- RULES:
62
- 1. search_content MUST preserve the original indentation/whitespace
63
- 2. file replacement can only be used for existing files marked as
64
- """
65
-
66
- def build_selected_option_prompt(option_text: str, request: str, files_content: str = "") -> str:
67
- """Build prompt for selected option details
68
-
69
- Args:
70
- option_text: Formatted text describing the selected option
71
- request: The original user request
72
- files_content: Content of relevant files
73
- """
74
- short_uuid = str(uuid.uuid4())[:8]
75
-
76
- return SELECTED_OPTION_PROMPT.format(
77
- option_text=option_text,
78
- request=request,
79
- files_content=files_content,
80
- uuid=short_uuid
81
- )
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
@@ -6,7 +6,8 @@ from rich.table import Table
6
6
  from rich.rule import Rule
7
7
  from janito.agents import AIAgent
8
8
  from janito.common import progress_send_message
9
- from janito.scan import show_content_stats
9
+ from janito.workspace import workspace
10
+
10
11
 
11
12
  QA_PROMPT = """Please provide a clear and concise answer to the following question about the codebase:
12
13
 
@@ -23,8 +24,8 @@ Format your response using markdown with appropriate headers and code blocks.
23
24
 
24
25
  def ask_question(question: str, files_content: str) -> str:
25
26
  """Process a question about the codebase and return the answer"""
26
- # Show content stats before processing
27
- show_content_stats(files_content)
27
+ # Analyze workspace content if needed
28
+ workspace.analyze()
28
29
 
29
30
  prompt = QA_PROMPT.format(
30
31
  question=question,
@@ -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,6 @@
1
+ from .core import SearchReplacer, PatternNotFoundException
2
+ from .searcher import Searcher
3
+ from .replacer import Replacer
4
+ from .parser import parse_test_file
5
+
6
+ __all__ = ['SearchReplacer', 'PatternNotFoundException', 'Searcher', 'Replacer', 'parse_test_file']
@@ -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)