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/cli/handlers/request.py
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from rich.console import Console
|
3
|
-
from janito.config import config
|
4
|
-
from janito.workspace import is_dir_empty
|
5
|
-
from janito.change.core import process_change_request
|
6
|
-
from ..base import BaseCLIHandler
|
7
|
-
|
8
|
-
class RequestHandler(BaseCLIHandler):
|
9
|
-
def handle(self, request: str, preview_only: bool = False):
|
10
|
-
"""Process a modification request"""
|
11
|
-
is_empty = is_dir_empty(config.workspace_dir)
|
12
|
-
if is_empty and not config.include:
|
13
|
-
self.console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
14
|
-
|
15
|
-
success, history_file = process_change_request(request, preview_only)
|
16
|
-
|
17
|
-
if success and history_file and config.verbose:
|
18
|
-
try:
|
19
|
-
rel_path = history_file.relative_to(config.workspace_dir)
|
20
|
-
self.console.print(f"\nChanges saved to: ./{rel_path}")
|
21
|
-
except ValueError:
|
22
|
-
self.console.print(f"\nChanges saved to: {history_file}")
|
23
|
-
elif not success:
|
24
|
-
self.console.print("[red]Failed to process change request[/red]")
|
janito/cli/handlers/scan.py
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import List
|
3
|
-
from janito.workspace import preview_scan
|
4
|
-
from ..base import BaseCLIHandler
|
5
|
-
|
6
|
-
class ScanHandler(BaseCLIHandler):
|
7
|
-
def handle(self, paths_to_scan: List[Path]):
|
8
|
-
"""Preview files that would be analyzed"""
|
9
|
-
preview_scan(paths_to_scan)
|
janito/cli/history.py
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from datetime import datetime, timezone
|
3
|
-
from typing import List
|
4
|
-
from rich.console import Console
|
5
|
-
from rich.table import Table
|
6
|
-
from janito.config import config
|
7
|
-
|
8
|
-
def get_history_path() -> Path:
|
9
|
-
"""Get the path to the history directory"""
|
10
|
-
history_dir = config.workspace_dir / '.janito' / 'history'
|
11
|
-
history_dir.mkdir(parents=True, exist_ok=True)
|
12
|
-
return history_dir
|
13
|
-
|
14
|
-
def save_to_history(request: str, response: str) -> None:
|
15
|
-
"""Save a request and its response to the history file"""
|
16
|
-
history_dir = get_history_path()
|
17
|
-
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S')
|
18
|
-
history_file = history_dir / f"{timestamp}_request.txt"
|
19
|
-
|
20
|
-
content = f"""Request: {request}
|
21
|
-
Timestamp: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}
|
22
|
-
|
23
|
-
Response:
|
24
|
-
{response}
|
25
|
-
"""
|
26
|
-
history_file.write_text(content)
|
27
|
-
|
28
|
-
def display_history() -> None:
|
29
|
-
"""Display the history of requests"""
|
30
|
-
console = Console()
|
31
|
-
history_dir = get_history_path()
|
32
|
-
|
33
|
-
if not history_dir.exists():
|
34
|
-
console.print("[yellow]No history found[/yellow]")
|
35
|
-
return
|
36
|
-
|
37
|
-
table = Table(title="Request History")
|
38
|
-
table.add_column("Timestamp", style="cyan")
|
39
|
-
table.add_column("Request", style="white")
|
40
|
-
table.add_column("File", style="dim")
|
41
|
-
|
42
|
-
history_files = sorted(history_dir.glob("*_request.txt"), reverse=True)
|
43
|
-
|
44
|
-
if not history_files:
|
45
|
-
console.print("[yellow]No history found[/yellow]")
|
46
|
-
return
|
47
|
-
|
48
|
-
for history_file in history_files:
|
49
|
-
try:
|
50
|
-
content = history_file.read_text()
|
51
|
-
request_line = next(line for line in content.splitlines() if line.startswith("Request:"))
|
52
|
-
timestamp_line = next(line for line in content.splitlines() if line.startswith("Timestamp:"))
|
53
|
-
|
54
|
-
request = request_line.replace("Request:", "").strip()
|
55
|
-
timestamp = timestamp_line.replace("Timestamp:", "").strip()
|
56
|
-
|
57
|
-
table.add_row(timestamp, request, history_file.name)
|
58
|
-
except Exception as e:
|
59
|
-
console.print(f"[red]Error reading {history_file}: {e}[/red]")
|
60
|
-
|
61
|
-
console.print(table)
|
janito/cli/registry.py
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
from typing import Callable, Dict
|
2
|
-
from dataclasses import dataclass
|
3
|
-
|
4
|
-
@dataclass
|
5
|
-
class Command:
|
6
|
-
name: str
|
7
|
-
handler: Callable
|
8
|
-
help_text: str
|
9
|
-
|
10
|
-
class CommandRegistry:
|
11
|
-
def __init__(self):
|
12
|
-
self._commands: Dict[str, Command] = {}
|
13
|
-
|
14
|
-
def register(self, name: str, help_text: str):
|
15
|
-
def decorator(handler: Callable):
|
16
|
-
self._commands[name] = Command(name, handler, help_text)
|
17
|
-
return handler
|
18
|
-
return decorator
|
19
|
-
|
20
|
-
def get_command(self, name: str) -> Command:
|
21
|
-
return self._commands.get(name)
|
22
|
-
|
23
|
-
def get_all_commands(self) -> Dict[str, Command]:
|
24
|
-
return self._commands
|
25
|
-
|
26
|
-
registry = CommandRegistry()
|
janito/demo/__init__.py
DELETED
janito/demo/data.py
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
from typing import Dict, List
|
2
|
-
from .scenarios import DemoScenario
|
3
|
-
from .mock_data import get_mock_changes
|
4
|
-
|
5
|
-
def get_demo_scenarios() -> List[DemoScenario]:
|
6
|
-
"""Get list of predefined demo scenarios"""
|
7
|
-
return [
|
8
|
-
DemoScenario(
|
9
|
-
name="File Operations Demo",
|
10
|
-
description="Demonstrate various file operations with change viewer",
|
11
|
-
changes=get_mock_changes()
|
12
|
-
)
|
13
|
-
]
|
janito/demo/mock_data.py
DELETED
@@ -1,20 +0,0 @@
|
|
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
|
-
]
|
janito/demo/operations.py
DELETED
@@ -1,45 +0,0 @@
|
|
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
DELETED
@@ -1,59 +0,0 @@
|
|
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)
|
janito/demo/scenarios.py
DELETED
@@ -1,32 +0,0 @@
|
|
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
DELETED
janito/review.py
DELETED
@@ -1,13 +0,0 @@
|
|
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))
|
janito/search_replace/README.md
DELETED
@@ -1,146 +0,0 @@
|
|
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
|
@@ -1,21 +0,0 @@
|
|
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()
|
janito/search_replace/core.py
DELETED
@@ -1,119 +0,0 @@
|
|
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)
|
janito/search_replace/parser.py
DELETED
@@ -1,52 +0,0 @@
|
|
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 []
|