janito 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
@@ -0,0 +1,411 @@
|
|
1
|
+
from typing import List, Optional, Dict, Type
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
import re
|
4
|
+
from .strategy_result import StrategyResult
|
5
|
+
|
6
|
+
LINE_OVER_LINE_DEBUG = False
|
7
|
+
|
8
|
+
class SearchStrategy(ABC):
|
9
|
+
"""Base class for search strategies."""
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
"""Initialize strategy with name derived from class name."""
|
13
|
+
self.name = self.__class__.__name__.replace('Strategy', '')
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
17
|
+
"""Check if pattern matches source at given position.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
source_lines: List of source code lines to search in
|
21
|
+
pattern_lines: List of pattern lines to match
|
22
|
+
pos: Position in source_lines to start matching
|
23
|
+
searcher: Searcher instance for utility methods
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
bool: True if pattern matches at position, False otherwise
|
27
|
+
"""
|
28
|
+
pass
|
29
|
+
|
30
|
+
class ExactMatchStrategy(SearchStrategy):
|
31
|
+
"""Strategy for exact match including indentation."""
|
32
|
+
|
33
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
34
|
+
"""Match pattern exactly with indentation.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
source_lines: List of source code lines to search in
|
38
|
+
pattern_lines: List of pattern lines to match
|
39
|
+
pos: Position in source_lines to start matching
|
40
|
+
searcher: Searcher instance for utility methods
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
bool: True if pattern matches exactly at position, False otherwise
|
44
|
+
"""
|
45
|
+
if pos + len(pattern_lines) > len(source_lines):
|
46
|
+
return False
|
47
|
+
return all(source_lines[pos + i] == pattern_line
|
48
|
+
for i, pattern_line in enumerate(pattern_lines))
|
49
|
+
|
50
|
+
class ExactContentStrategy(SearchStrategy):
|
51
|
+
"""Exact content match ignoring all indentation."""
|
52
|
+
|
53
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
54
|
+
"""Match pattern exactly ignoring indentation.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
source_lines: List of source code lines to search in
|
58
|
+
pattern_lines: List of pattern lines to match
|
59
|
+
pos: Position in source_lines to start matching
|
60
|
+
searcher: Searcher instance for utility methods
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
bool: True if pattern matches exactly at position, False otherwise
|
64
|
+
"""
|
65
|
+
if pos + len(pattern_lines) > len(source_lines):
|
66
|
+
return False
|
67
|
+
return all(source_lines[pos + i].strip() == pattern_line.strip()
|
68
|
+
for i, pattern_line in enumerate(pattern_lines)
|
69
|
+
if pattern_line.strip())
|
70
|
+
|
71
|
+
class IndentAwareStrategy(SearchStrategy):
|
72
|
+
"""Indentation-aware matching preserving relative indentation."""
|
73
|
+
|
74
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
75
|
+
"""Match pattern preserving relative indentation.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
source_lines: List of source code lines to search in
|
79
|
+
pattern_lines: List of pattern lines to match
|
80
|
+
pos: Position in source_lines to start matching
|
81
|
+
searcher: Searcher instance for utility methods
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
bool: True if pattern matches preserving indentation at position, False otherwise
|
85
|
+
"""
|
86
|
+
if pos + len(pattern_lines) > len(source_lines):
|
87
|
+
return False
|
88
|
+
match_indent = searcher.get_indentation(source_lines[pos])
|
89
|
+
return all(source_lines[pos + i].startswith(match_indent + pattern_line)
|
90
|
+
for i, pattern_line in enumerate(pattern_lines)
|
91
|
+
if pattern_line.strip())
|
92
|
+
|
93
|
+
class ExactContentNoComments(SearchStrategy):
|
94
|
+
"""Exact content match ignoring indentation, comments, and empty lines."""
|
95
|
+
|
96
|
+
def _strip_comments(self, line: str) -> str:
|
97
|
+
"""Remove comments from line."""
|
98
|
+
if '#' in line:
|
99
|
+
line = line.split('#')[0]
|
100
|
+
if '//' in line:
|
101
|
+
line = line.split('//')[0]
|
102
|
+
return line.strip()
|
103
|
+
|
104
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
105
|
+
"""Match pattern ignoring comments and empty lines.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
source_lines: List of source code lines to search in
|
109
|
+
pattern_lines: List of pattern lines to match
|
110
|
+
pos: Position in source_lines to start matching
|
111
|
+
searcher: Searcher instance for utility methods
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
bool: True if pattern matches ignoring comments at position, False otherwise
|
115
|
+
"""
|
116
|
+
if pos + len(pattern_lines) > len(source_lines):
|
117
|
+
return False
|
118
|
+
|
119
|
+
if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
|
120
|
+
print("\n[DEBUG] ExactContentNoComments trying to match at line", pos + 1)
|
121
|
+
|
122
|
+
# Filter out comments and empty lines from pattern
|
123
|
+
pattern_content = [self._strip_comments(line) for line in pattern_lines]
|
124
|
+
pattern_content = [line for line in pattern_content if line]
|
125
|
+
|
126
|
+
if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
|
127
|
+
print("[DEBUG] Pattern after processing:")
|
128
|
+
for i, line in enumerate(pattern_content):
|
129
|
+
print(f"[DEBUG] {i+1}: '{line}'")
|
130
|
+
|
131
|
+
# Match against source, ignoring comments and empty lines
|
132
|
+
source_idx = pos
|
133
|
+
pattern_idx = 0
|
134
|
+
|
135
|
+
while pattern_idx < len(pattern_content) and source_idx < len(source_lines):
|
136
|
+
source_line = self._strip_comments(source_lines[source_idx])
|
137
|
+
if not source_line:
|
138
|
+
source_idx += 1
|
139
|
+
continue
|
140
|
+
|
141
|
+
if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
|
142
|
+
print(f"[DEBUG] Line {source_idx + 1}: '{source_line}' vs '{pattern_content[pattern_idx]}'")
|
143
|
+
|
144
|
+
if source_line != pattern_content[pattern_idx]:
|
145
|
+
if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
|
146
|
+
print("[DEBUG] Line mismatch")
|
147
|
+
return False
|
148
|
+
|
149
|
+
pattern_idx += 1
|
150
|
+
source_idx += 1
|
151
|
+
|
152
|
+
match_result = pattern_idx == len(pattern_content)
|
153
|
+
if match_result and searcher.debug_mode:
|
154
|
+
print("[DEBUG] Match found")
|
155
|
+
return True
|
156
|
+
|
157
|
+
return False
|
158
|
+
|
159
|
+
class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
|
160
|
+
"""Match first line partially, ignoring comments."""
|
161
|
+
|
162
|
+
def _strip_comments(self, line: str) -> str:
|
163
|
+
"""Remove comments from line."""
|
164
|
+
if '#' in line:
|
165
|
+
line = line.split('#')[0]
|
166
|
+
if '//' in line:
|
167
|
+
line = line.split('//')[0]
|
168
|
+
return line.strip()
|
169
|
+
|
170
|
+
def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
|
171
|
+
"""Match first line of pattern partially ignoring comments.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
source_lines: List of source code lines to search in
|
175
|
+
pattern_lines: List of pattern lines to match
|
176
|
+
pos: Position in source_lines to start matching
|
177
|
+
searcher: Searcher instance for utility methods
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
bool: True if first line of pattern matches partially at position, False otherwise
|
181
|
+
"""
|
182
|
+
if pos >= len(source_lines):
|
183
|
+
return False
|
184
|
+
|
185
|
+
# Get first non-empty pattern line
|
186
|
+
pattern_content = []
|
187
|
+
for line in pattern_lines:
|
188
|
+
stripped = self._strip_comments(line)
|
189
|
+
if stripped:
|
190
|
+
pattern_content.append(stripped)
|
191
|
+
break
|
192
|
+
|
193
|
+
if not pattern_content:
|
194
|
+
return False
|
195
|
+
|
196
|
+
# Get source line content
|
197
|
+
source_line = self._strip_comments(source_lines[pos])
|
198
|
+
if not source_line:
|
199
|
+
return False
|
200
|
+
|
201
|
+
# Check if pattern content is part of source line
|
202
|
+
return pattern_content[0] in source_line
|
203
|
+
|
204
|
+
class Searcher:
|
205
|
+
"""Handles pattern searching in source code with configurable strategies."""
|
206
|
+
|
207
|
+
def __init__(self, debug: bool = False):
|
208
|
+
"""Initialize searcher with debug mode."""
|
209
|
+
self.debug_mode = debug
|
210
|
+
|
211
|
+
@classmethod
|
212
|
+
def set_debug(cls, enabled: bool):
|
213
|
+
"""Enable or disable debug mode - deprecated, use instance property instead"""
|
214
|
+
# Remove the class-level debug setting as it's no longer needed
|
215
|
+
raise DeprecationWarning("Class-level debug setting is deprecated. Use instance debug_mode property instead.")
|
216
|
+
|
217
|
+
# Updated extension to strategy mapping to include ExactContentNoComments
|
218
|
+
EXTENSION_STRATEGIES = {
|
219
|
+
'.py': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
|
220
|
+
'.java': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
|
221
|
+
'.js': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
|
222
|
+
'.ts': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
|
223
|
+
'*': [ExactMatchStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()] # updated default fallback
|
224
|
+
}
|
225
|
+
|
226
|
+
def get_strategies(self, file_ext: Optional[str]) -> List[SearchStrategy]:
|
227
|
+
"""Get search strategies for given file extension."""
|
228
|
+
if not file_ext:
|
229
|
+
return self.EXTENSION_STRATEGIES['*']
|
230
|
+
return self.EXTENSION_STRATEGIES.get(file_ext.lower(), self.EXTENSION_STRATEGIES['*'])
|
231
|
+
|
232
|
+
@staticmethod
|
233
|
+
def get_indentation(line: str) -> str:
|
234
|
+
"""Get the leading whitespace of a line."""
|
235
|
+
return re.match(r'^[ \t]*', line).group()
|
236
|
+
|
237
|
+
@staticmethod
|
238
|
+
def get_first_non_empty_line(text: str) -> tuple[str, int]:
|
239
|
+
"""Get first non-empty line and its index."""
|
240
|
+
lines = text.splitlines()
|
241
|
+
for i, line in enumerate(lines):
|
242
|
+
if line.strip():
|
243
|
+
return line, i
|
244
|
+
return '', 0
|
245
|
+
|
246
|
+
@staticmethod
|
247
|
+
def get_last_non_empty_line(text: str) -> tuple[str, int]:
|
248
|
+
"""Get last non-empty line and its index."""
|
249
|
+
lines = text.splitlines()
|
250
|
+
for i in range(len(lines) - 1, -1, -1):
|
251
|
+
if lines[i].strip():
|
252
|
+
return lines[i], i
|
253
|
+
return '', 0
|
254
|
+
|
255
|
+
def _build_indent_map(self, text: str) -> dict[int, int]:
|
256
|
+
"""Build a map of line numbers to their indentation levels.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
text: Source text to analyze
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
dict[int, int]: Mapping of line numbers to indentation levels
|
263
|
+
"""
|
264
|
+
indent_map = {}
|
265
|
+
for i, line in enumerate(text.splitlines()):
|
266
|
+
if line.strip(): # Only track non-empty lines
|
267
|
+
indent_map[i] = len(self.get_indentation(line))
|
268
|
+
if self.debug_mode:
|
269
|
+
print(f"[DEBUG] Line {i}: Indentation level {indent_map[i]}")
|
270
|
+
return indent_map
|
271
|
+
|
272
|
+
def normalize_pattern(self, pattern: str, base_indent: str = '') -> str:
|
273
|
+
"""Remove base indentation from pattern to help with matching."""
|
274
|
+
lines = pattern.splitlines()
|
275
|
+
first_line, start_idx = self.get_first_non_empty_line(pattern)
|
276
|
+
last_line, end_idx = self.get_last_non_empty_line(pattern)
|
277
|
+
|
278
|
+
# Calculate minimum indentation from first and last non-empty lines
|
279
|
+
first_indent = len(self.get_indentation(first_line))
|
280
|
+
last_indent = len(self.get_indentation(last_line))
|
281
|
+
min_indent = min(first_indent, last_indent)
|
282
|
+
|
283
|
+
if self.debug_mode:
|
284
|
+
print(f"[DEBUG] First line indent: {first_indent}")
|
285
|
+
print(f"[DEBUG] Last line indent: {last_indent}")
|
286
|
+
print(f"[DEBUG] Using minimum indent: {min_indent}")
|
287
|
+
|
288
|
+
normalized = []
|
289
|
+
for i, line in enumerate(lines):
|
290
|
+
if not line.strip():
|
291
|
+
normalized.append('')
|
292
|
+
else:
|
293
|
+
line_indent = len(self.get_indentation(line))
|
294
|
+
if line_indent < min_indent:
|
295
|
+
if self.debug_mode:
|
296
|
+
print(f"[DEBUG] Warning: Line {i} has less indentation ({line_indent}) than minimum ({min_indent})")
|
297
|
+
normalized.append(line)
|
298
|
+
else:
|
299
|
+
normalized.append(line[min_indent:])
|
300
|
+
if self.debug_mode:
|
301
|
+
print(f"[DEBUG] Normalized line {i}: '{normalized[-1]}'")
|
302
|
+
|
303
|
+
return '\n'.join(normalized)
|
304
|
+
|
305
|
+
def _find_best_match_position(self, positions: List[int], source_lines: List[str], pattern_base_indent: int) -> Optional[int]:
|
306
|
+
"""Find the best matching position among candidates.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
positions: List of candidate line positions
|
310
|
+
source_lines: List of source code lines
|
311
|
+
pattern_base_indent: Base indentation level of pattern
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
Optional[int]: Best matching position or None if no matches
|
315
|
+
"""
|
316
|
+
if self.debug_mode:
|
317
|
+
print(f"[DEBUG] Finding best match among positions: {[p+1 for p in positions]}") # Show 1-based line numbers
|
318
|
+
|
319
|
+
if not positions:
|
320
|
+
return None
|
321
|
+
|
322
|
+
best_pos = min(positions) # Simply take the earliest match
|
323
|
+
if self.debug_mode:
|
324
|
+
print(f"[DEBUG] Selected match at line {best_pos + 1}") # Show 1-based line number
|
325
|
+
return best_pos
|
326
|
+
|
327
|
+
def try_match_with_strategies(self, source_lines: List[str], pattern_lines: List[str],
|
328
|
+
pos: int, strategies: List[SearchStrategy]) -> StrategyResult:
|
329
|
+
"""Try matching using multiple strategies in sequence.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
source_lines: List of source code lines
|
333
|
+
pattern_lines: List of pattern lines to match
|
334
|
+
pos: Position to start matching
|
335
|
+
strategies: List of strategies to try
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
StrategyResult: Result containing match success and strategy used
|
339
|
+
"""
|
340
|
+
if self.debug_mode and LINE_OVER_LINE_DEBUG:
|
341
|
+
print(f"\n[DEBUG] Trying to match at line {pos + 1}")
|
342
|
+
|
343
|
+
for strategy in strategies:
|
344
|
+
if strategy.match(source_lines, pattern_lines, pos, self):
|
345
|
+
if self.debug_mode:
|
346
|
+
print(f"[DEBUG] Match found with {strategy.__class__.__name__}")
|
347
|
+
print(f"[DEBUG] Stopping strategy chain at line {pos + 1}")
|
348
|
+
return StrategyResult(success=True, strategy_name=strategy.name, match_position=pos)
|
349
|
+
return StrategyResult(success=False)
|
350
|
+
|
351
|
+
def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
|
352
|
+
file_ext: Optional[str] = None) -> List[int]:
|
353
|
+
"""Find all matching positions using available strategies.
|
354
|
+
|
355
|
+
Args:
|
356
|
+
source_lines: List of source code lines
|
357
|
+
pattern_lines: List of pattern lines to match
|
358
|
+
file_ext: Optional file extension to determine strategies
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
List[int]: List of matching line positions
|
362
|
+
"""
|
363
|
+
strategies = self.get_strategies(file_ext)
|
364
|
+
|
365
|
+
if self.debug_mode:
|
366
|
+
print("\nTrying search strategies:")
|
367
|
+
print("-" * 50)
|
368
|
+
|
369
|
+
# Track positions already matched to avoid redundant attempts
|
370
|
+
matched_positions = set()
|
371
|
+
all_matches = []
|
372
|
+
|
373
|
+
for strategy in strategies:
|
374
|
+
strategy_name = strategy.__class__.__name__.replace('Strategy', '')
|
375
|
+
|
376
|
+
if self.debug_mode:
|
377
|
+
print(f"\n→ {strategy_name}...")
|
378
|
+
|
379
|
+
for i in range(len(source_lines)):
|
380
|
+
if i in matched_positions:
|
381
|
+
continue
|
382
|
+
|
383
|
+
if strategy.match(source_lines, pattern_lines, i, self):
|
384
|
+
matched_positions.add(i)
|
385
|
+
all_matches.append(i)
|
386
|
+
if self.debug_mode:
|
387
|
+
print(f"✓ Match found at line {i+1} using {strategy_name}")
|
388
|
+
|
389
|
+
if all_matches and isinstance(strategy, ExactMatchStrategy):
|
390
|
+
# If we found exact matches, no need to try other strategies
|
391
|
+
break
|
392
|
+
|
393
|
+
if self.debug_mode and all_matches:
|
394
|
+
print(f"\nFound {len(all_matches)} total match(es) at line(s) {[m+1 for m in sorted(all_matches)]}")
|
395
|
+
|
396
|
+
return sorted(all_matches)
|
397
|
+
|
398
|
+
def _check_exact_match(self, source_lines: List[str], pattern_lines: List[str], pos: int) -> bool:
|
399
|
+
"""Check for exact line-by-line match at position.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
source_lines: List of source code lines
|
403
|
+
pattern_lines: List of pattern lines to match
|
404
|
+
pos: Position to check for match
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
bool: True if exact match found, False otherwise
|
408
|
+
"""
|
409
|
+
if pos + len(pattern_lines) > len(source_lines):
|
410
|
+
return False
|
411
|
+
return all(source_lines[pos + j] == pattern_lines[j] for j in range(len(pattern_lines)))
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
@dataclass
|
5
|
+
class StrategyResult:
|
6
|
+
"""Encapsulates the result of a strategy match attempt."""
|
7
|
+
success: bool
|
8
|
+
strategy_name: Optional[str] = None
|
9
|
+
match_position: Optional[int] = None
|
10
|
+
file_type: Optional[str] = None
|
janito/shell/__init__.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
"""Shell package initialization for Janito."""
|
2
|
+
from typing import Optional
|
3
|
+
from prompt_toolkit import PromptSession
|
4
|
+
from rich.console import Console
|
5
|
+
from janito.config import config
|
6
|
+
from janito.workspace.workset import Workset
|
7
|
+
from .processor import CommandProcessor
|
8
|
+
from .commands import register_commands
|
9
|
+
from .registry import CommandRegistry
|
10
|
+
|
11
|
+
def start_shell() -> None:
|
12
|
+
"""Start the Janito interactive shell."""
|
13
|
+
# Create single registry instance
|
14
|
+
registry = CommandRegistry()
|
15
|
+
register_commands(registry)
|
16
|
+
|
17
|
+
# Create shell components with shared registry
|
18
|
+
from .prompt import create_shell_session
|
19
|
+
session = create_shell_session(registry)
|
20
|
+
processor = CommandProcessor(registry)
|
21
|
+
|
22
|
+
# Initialize and show workset content
|
23
|
+
workset = Workset()
|
24
|
+
workset.refresh()
|
25
|
+
workset.show()
|
26
|
+
|
27
|
+
while True:
|
28
|
+
try:
|
29
|
+
text = session.prompt("janito🤖 ")
|
30
|
+
if text.strip():
|
31
|
+
processor.process_command(text)
|
32
|
+
except KeyboardInterrupt:
|
33
|
+
continue
|
34
|
+
except EOFError:
|
35
|
+
break
|
36
|
+
except Exception as e:
|
37
|
+
print(f"Error: {e}")
|
38
|
+
print("Goodbye!")
|
janito/shell/bus.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Command bus implementation for Janito shell."""
|
2
|
+
from typing import Dict, Callable, Any, Optional
|
3
|
+
from dataclasses import dataclass
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class Command:
|
7
|
+
"""Command message for command bus."""
|
8
|
+
name: str
|
9
|
+
args: str
|
10
|
+
|
11
|
+
class CommandBus:
|
12
|
+
"""Simple command bus implementation."""
|
13
|
+
_instance = None
|
14
|
+
_handlers: Dict[str, Callable[[Command], None]] = {}
|
15
|
+
|
16
|
+
def __new__(cls):
|
17
|
+
if cls._instance is None:
|
18
|
+
cls._instance = super().__new__(cls)
|
19
|
+
cls._instance._handlers = {}
|
20
|
+
return cls._instance
|
21
|
+
|
22
|
+
def register_handler(self, command_name: str, handler: Callable[[Command], None]) -> None:
|
23
|
+
"""Register a handler for a command."""
|
24
|
+
self._handlers[command_name] = handler
|
25
|
+
|
26
|
+
def handle(self, command: Command) -> None:
|
27
|
+
"""Handle a command by dispatching to appropriate handler."""
|
28
|
+
if handler := self._handlers.get(command.name):
|
29
|
+
handler(command)
|
30
|
+
else:
|
31
|
+
raise ValueError(f"No handler registered for command: {command.name}")
|