janito 0.6.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/__main__.py +37 -30
- janito/agents/__init__.py +8 -2
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +13 -23
- janito/agents/openai.py +5 -1
- janito/change/analysis/analyze.py +8 -7
- janito/change/analysis/prompts.py +4 -12
- janito/change/analysis/view/terminal.py +21 -11
- janito/change/applier/text.py +7 -5
- janito/change/core.py +22 -29
- janito/change/parser.py +0 -2
- janito/change/prompts.py +16 -21
- janito/change/validator.py +27 -9
- janito/change/viewer/content.py +1 -1
- janito/change/viewer/panels.py +93 -115
- janito/change/viewer/styling.py +15 -4
- janito/cli/commands.py +63 -20
- janito/common.py +44 -18
- janito/config.py +44 -44
- janito/prompt.py +36 -0
- janito/qa.py +5 -14
- janito/search_replace/README.md +63 -17
- janito/search_replace/__init__.py +2 -1
- janito/search_replace/core.py +15 -14
- janito/search_replace/logger.py +35 -0
- janito/search_replace/searcher.py +160 -48
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +15 -16
- janito/shell/commands.py +38 -97
- janito/shell/processor.py +7 -27
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/workspace/__init__.py +4 -5
- janito/workspace/analysis.py +2 -2
- 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.6.0.dist-info → janito-0.7.0.dist-info}/RECORD +44 -43
- janito/change/viewer/pager.py +0 -56
- 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/prompts.py +0 -2
- janito/shell/handlers.py +0 -122
- 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 → janito-0.7.0.dist-info}/WHEEL +0 -0
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,47 @@
|
|
1
1
|
from typing import List, Optional, Dict, Type
|
2
2
|
from abc import ABC, abstractmethod
|
3
3
|
import re
|
4
|
+
from .strategy_result import StrategyResult
|
4
5
|
|
5
6
|
LINE_OVER_LINE_DEBUG = False
|
6
7
|
|
7
8
|
class SearchStrategy(ABC):
|
8
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
|
+
|
9
15
|
@abstractmethod
|
10
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
|
+
"""
|
11
28
|
pass
|
12
29
|
|
13
30
|
class ExactMatchStrategy(SearchStrategy):
|
14
|
-
"""
|
31
|
+
"""Strategy for exact match including indentation."""
|
32
|
+
|
15
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
|
+
"""
|
16
45
|
if pos + len(pattern_lines) > len(source_lines):
|
17
46
|
return False
|
18
47
|
return all(source_lines[pos + i] == pattern_line
|
@@ -20,7 +49,19 @@ class ExactMatchStrategy(SearchStrategy):
|
|
20
49
|
|
21
50
|
class ExactContentStrategy(SearchStrategy):
|
22
51
|
"""Exact content match ignoring all indentation."""
|
52
|
+
|
23
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
|
+
"""
|
24
65
|
if pos + len(pattern_lines) > len(source_lines):
|
25
66
|
return False
|
26
67
|
return all(source_lines[pos + i].strip() == pattern_line.strip()
|
@@ -29,7 +70,19 @@ class ExactContentStrategy(SearchStrategy):
|
|
29
70
|
|
30
71
|
class IndentAwareStrategy(SearchStrategy):
|
31
72
|
"""Indentation-aware matching preserving relative indentation."""
|
73
|
+
|
32
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
|
+
"""
|
33
86
|
if pos + len(pattern_lines) > len(source_lines):
|
34
87
|
return False
|
35
88
|
match_indent = searcher.get_indentation(source_lines[pos])
|
@@ -39,6 +92,7 @@ class IndentAwareStrategy(SearchStrategy):
|
|
39
92
|
|
40
93
|
class ExactContentNoComments(SearchStrategy):
|
41
94
|
"""Exact content match ignoring indentation, comments, and empty lines."""
|
95
|
+
|
42
96
|
def _strip_comments(self, line: str) -> str:
|
43
97
|
"""Remove comments from line."""
|
44
98
|
if '#' in line:
|
@@ -48,6 +102,17 @@ class ExactContentNoComments(SearchStrategy):
|
|
48
102
|
return line.strip()
|
49
103
|
|
50
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
|
+
"""
|
51
116
|
if pos + len(pattern_lines) > len(source_lines):
|
52
117
|
return False
|
53
118
|
|
@@ -93,6 +158,7 @@ class ExactContentNoComments(SearchStrategy):
|
|
93
158
|
|
94
159
|
class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
|
95
160
|
"""Match first line partially, ignoring comments."""
|
161
|
+
|
96
162
|
def _strip_comments(self, line: str) -> str:
|
97
163
|
"""Remove comments from line."""
|
98
164
|
if '#' in line:
|
@@ -102,6 +168,17 @@ class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
|
|
102
168
|
return line.strip()
|
103
169
|
|
104
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
|
+
"""
|
105
182
|
if pos >= len(source_lines):
|
106
183
|
return False
|
107
184
|
|
@@ -176,7 +253,14 @@ class Searcher:
|
|
176
253
|
return '', 0
|
177
254
|
|
178
255
|
def _build_indent_map(self, text: str) -> dict[int, int]:
|
179
|
-
"""Build a map of line numbers to indentation levels.
|
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
|
+
"""
|
180
264
|
indent_map = {}
|
181
265
|
for i, line in enumerate(text.splitlines()):
|
182
266
|
if line.strip(): # Only track non-empty lines
|
@@ -219,7 +303,16 @@ class Searcher:
|
|
219
303
|
return '\n'.join(normalized)
|
220
304
|
|
221
305
|
def _find_best_match_position(self, positions: List[int], source_lines: List[str], pattern_base_indent: int) -> Optional[int]:
|
222
|
-
"""Find the best matching position
|
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
|
+
"""
|
223
316
|
if self.debug_mode:
|
224
317
|
print(f"[DEBUG] Finding best match among positions: {[p+1 for p in positions]}") # Show 1-based line numbers
|
225
318
|
|
@@ -231,69 +324,88 @@ class Searcher:
|
|
231
324
|
print(f"[DEBUG] Selected match at line {best_pos + 1}") # Show 1-based line number
|
232
325
|
return best_pos
|
233
326
|
|
234
|
-
def try_match_with_strategies(self, source_lines: List[str], pattern_lines: List[str],
|
235
|
-
pos: int, strategies: List[SearchStrategy]) ->
|
236
|
-
"""Try matching using multiple strategies in
|
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
|
+
"""
|
237
340
|
if self.debug_mode and LINE_OVER_LINE_DEBUG:
|
238
341
|
print(f"\n[DEBUG] Trying to match at line {pos + 1}")
|
239
|
-
|
342
|
+
|
240
343
|
for strategy in strategies:
|
241
344
|
if strategy.match(source_lines, pattern_lines, pos, self):
|
242
345
|
if self.debug_mode:
|
243
346
|
print(f"[DEBUG] Match found with {strategy.__class__.__name__}")
|
244
347
|
print(f"[DEBUG] Stopping strategy chain at line {pos + 1}")
|
245
|
-
return True
|
246
|
-
return False
|
348
|
+
return StrategyResult(success=True, strategy_name=strategy.name, match_position=pos)
|
349
|
+
return StrategyResult(success=False)
|
247
350
|
|
248
|
-
def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
|
351
|
+
def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
|
249
352
|
file_ext: Optional[str] = None) -> List[int]:
|
250
|
-
"""Find all
|
251
|
-
strategies = self.get_strategies(file_ext)
|
353
|
+
"""Find all matching positions using available strategies.
|
252
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
|
+
|
253
365
|
if self.debug_mode:
|
254
366
|
print("\nTrying search strategies:")
|
255
367
|
print("-" * 50)
|
256
|
-
|
257
|
-
#
|
368
|
+
|
369
|
+
# Track positions already matched to avoid redundant attempts
|
370
|
+
matched_positions = set()
|
371
|
+
all_matches = []
|
372
|
+
|
258
373
|
for strategy in strategies:
|
259
|
-
matches = []
|
260
374
|
strategy_name = strategy.__class__.__name__.replace('Strategy', '')
|
261
|
-
|
375
|
+
|
262
376
|
if self.debug_mode:
|
263
377
|
print(f"\n→ {strategy_name}...")
|
264
|
-
|
378
|
+
|
265
379
|
for i in range(len(source_lines)):
|
380
|
+
if i in matched_positions:
|
381
|
+
continue
|
382
|
+
|
266
383
|
if strategy.match(source_lines, pattern_lines, i, self):
|
267
|
-
|
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}")
|
268
388
|
|
269
|
-
if
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
for i in range(len(source_lines) - pattern_len + 1):
|
287
|
-
is_match = True
|
288
|
-
for j in range(pattern_len):
|
289
|
-
if source_lines[i + j] != pattern_lines[j]:
|
290
|
-
is_match = False
|
291
|
-
break
|
292
|
-
|
293
|
-
if is_match:
|
294
|
-
matches.append(i)
|
295
|
-
|
296
|
-
if self.debug_mode and matches:
|
297
|
-
print(f"[DEBUG] Found {len(matches)} exact matches")
|
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
|
298
405
|
|
299
|
-
|
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
CHANGED
@@ -1,29 +1,28 @@
|
|
1
1
|
"""Shell package initialization for Janito."""
|
2
2
|
from typing import Optional
|
3
3
|
from prompt_toolkit import PromptSession
|
4
|
-
from prompt_toolkit.history import FileHistory
|
5
|
-
from pathlib import Path
|
6
4
|
from rich.console import Console
|
7
5
|
from janito.config import config
|
8
|
-
from janito.workspace import
|
6
|
+
from janito.workspace.workset import Workset
|
9
7
|
from .processor import CommandProcessor
|
8
|
+
from .commands import register_commands
|
9
|
+
from .registry import CommandRegistry
|
10
10
|
|
11
11
|
def start_shell() -> None:
|
12
12
|
"""Start the Janito interactive shell."""
|
13
|
-
|
14
|
-
|
15
|
-
|
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)
|
16
21
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
scan_paths = config.include if config.include else [config.workspace_dir]
|
22
|
-
workspace.collect_content(scan_paths)
|
23
|
-
workspace.analyze()
|
24
|
-
|
25
|
-
# Store workspace content in processor for session
|
26
|
-
processor.workspace_content = workspace.get_content()
|
22
|
+
# Initialize and show workset content
|
23
|
+
workset = Workset()
|
24
|
+
workset.refresh()
|
25
|
+
workset.show()
|
27
26
|
|
28
27
|
while True:
|
29
28
|
try:
|
janito/shell/commands.py
CHANGED
@@ -1,52 +1,15 @@
|
|
1
1
|
"""Command system for Janito shell."""
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import Optional, Callable, Dict
|
4
|
-
from pathlib import Path
|
5
2
|
from rich.console import Console
|
6
3
|
from rich.table import Table
|
7
4
|
from prompt_toolkit import PromptSession
|
8
5
|
from prompt_toolkit.shortcuts import clear as ptk_clear
|
6
|
+
from prompt_toolkit.completion import PathCompleter
|
7
|
+
from prompt_toolkit.document import Document
|
8
|
+
from pathlib import Path
|
9
9
|
from janito.config import config
|
10
|
-
from janito.workspace import
|
10
|
+
from janito.workspace import workset # Updated import
|
11
11
|
from janito.workspace.analysis import analyze_workspace_content
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class Command:
|
15
|
-
"""Command definition with handler."""
|
16
|
-
name: str
|
17
|
-
description: str
|
18
|
-
usage: Optional[str]
|
19
|
-
handler: Callable[[str], None]
|
20
|
-
|
21
|
-
class CommandSystem:
|
22
|
-
"""Centralized command management system."""
|
23
|
-
_instance = None
|
24
|
-
_commands: Dict[str, Command] = {}
|
25
|
-
|
26
|
-
def __new__(cls):
|
27
|
-
if cls._instance is None:
|
28
|
-
cls._instance = super().__new__(cls)
|
29
|
-
cls._instance._commands = {}
|
30
|
-
return cls._instance
|
31
|
-
|
32
|
-
def register(self, command: Command) -> None:
|
33
|
-
"""Register a command."""
|
34
|
-
self._commands[command.name] = command
|
35
|
-
|
36
|
-
def get_command(self, name: str) -> Optional[Command]:
|
37
|
-
"""Get a command by name."""
|
38
|
-
return self._commands.get(name)
|
39
|
-
|
40
|
-
def get_commands(self) -> Dict[str, Command]:
|
41
|
-
"""Get all registered commands."""
|
42
|
-
return self._commands.copy()
|
43
|
-
|
44
|
-
def register_alias(self, alias: str, command_name: str) -> None:
|
45
|
-
"""Register an alias for a command."""
|
46
|
-
if command := self.get_command(command_name):
|
47
|
-
if alias in self._commands:
|
48
|
-
raise ValueError(f"Alias '{alias}' already registered")
|
49
|
-
self._commands[alias] = command
|
12
|
+
from .registry import CommandRegistry, Command, get_path_completer
|
50
13
|
|
51
14
|
def handle_request(args: str) -> None:
|
52
15
|
"""Handle a change request."""
|
@@ -75,23 +38,21 @@ def handle_ask(args: str) -> None:
|
|
75
38
|
def handle_help(args: str) -> None:
|
76
39
|
"""Handle help command."""
|
77
40
|
console = Console()
|
78
|
-
|
79
|
-
|
41
|
+
registry = CommandRegistry()
|
80
42
|
command = args.strip() if args else None
|
81
|
-
if command and (cmd :=
|
43
|
+
if command and (cmd := registry.get_command(command)):
|
82
44
|
console.print(f"\n[bold]{command}[/bold]: {cmd.description}")
|
83
45
|
if cmd.usage:
|
84
46
|
console.print(f"Usage: {cmd.usage}")
|
85
|
-
|
47
|
+
else:
|
48
|
+
table = Table(title="Available Commands")
|
49
|
+
table.add_column("Command", style="cyan")
|
50
|
+
table.add_column("Description")
|
86
51
|
|
87
|
-
|
88
|
-
|
89
|
-
table.add_column("Description")
|
52
|
+
for name, cmd in sorted(registry.get_commands().items()):
|
53
|
+
table.add_row(name, cmd.description)
|
90
54
|
|
91
|
-
|
92
|
-
table.add_row(name, cmd.description)
|
93
|
-
|
94
|
-
console.print(table)
|
55
|
+
console.print(table)
|
95
56
|
|
96
57
|
def handle_include(args: str) -> None:
|
97
58
|
"""Handle include command."""
|
@@ -116,42 +77,24 @@ def handle_include(args: str) -> None:
|
|
116
77
|
path = config.workspace_dir / path
|
117
78
|
resolved_paths.append(path.resolve())
|
118
79
|
|
119
|
-
|
120
|
-
|
121
|
-
analyze_workspace_content(content)
|
80
|
+
workset.include(resolved_paths)
|
81
|
+
workset.show()
|
122
82
|
|
123
83
|
console.print("[green]Updated include paths:[/green]")
|
124
84
|
for path in resolved_paths:
|
125
85
|
console.print(f" {path}")
|
126
86
|
|
127
|
-
from prompt_toolkit.completion import PathCompleter
|
128
|
-
from prompt_toolkit.document import Document
|
129
|
-
|
130
87
|
def handle_rinclude(args: str) -> None:
|
131
88
|
"""Handle recursive include command."""
|
132
89
|
console = Console()
|
133
90
|
session = PromptSession()
|
134
|
-
completer =
|
91
|
+
completer = get_path_completer(only_directories=True)
|
135
92
|
|
136
|
-
|
137
|
-
|
93
|
+
if not args:
|
94
|
+
try:
|
138
95
|
args = session.prompt("Enter directory paths (space separated): ", completer=completer)
|
139
|
-
|
140
|
-
|
141
|
-
doc = Document(args)
|
142
|
-
completions = list(completer.get_completions(doc, None))
|
143
|
-
if completions:
|
144
|
-
# If single completion, use it directly
|
145
|
-
if len(completions) == 1:
|
146
|
-
args = completions[0].text
|
147
|
-
else:
|
148
|
-
# Show available completions
|
149
|
-
console.print("\nAvailable directories:")
|
150
|
-
for comp in completions:
|
151
|
-
console.print(f" {comp.text}")
|
152
|
-
return
|
153
|
-
except (KeyboardInterrupt, EOFError):
|
154
|
-
return
|
96
|
+
except (KeyboardInterrupt, EOFError):
|
97
|
+
return
|
155
98
|
|
156
99
|
paths = [p.strip() for p in args.split() if p.strip()]
|
157
100
|
if not paths:
|
@@ -165,31 +108,29 @@ def handle_rinclude(args: str) -> None:
|
|
165
108
|
path = config.workspace_dir / path
|
166
109
|
resolved_paths.append(path.resolve())
|
167
110
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
111
|
+
workset.recursive(resolved_paths)
|
112
|
+
workset.include(resolved_paths) # Add recursive paths to include paths
|
113
|
+
workset.refresh()
|
114
|
+
workset.show()
|
172
115
|
|
173
116
|
console.print("[green]Updated recursive include paths:[/green]")
|
174
117
|
for path in resolved_paths:
|
175
118
|
console.print(f" {path}")
|
176
119
|
|
177
|
-
def register_commands() -> None:
|
120
|
+
def register_commands(registry: CommandRegistry) -> None:
|
178
121
|
"""Register all available commands."""
|
179
|
-
system = CommandSystem()
|
180
|
-
|
181
122
|
# Register main commands
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
123
|
+
registry.register(Command("/clear", "Clear the terminal screen", None, handle_clear))
|
124
|
+
registry.register(Command("/request", "Submit a change request", "/request <change request text>", handle_request))
|
125
|
+
registry.register(Command("/ask", "Ask a question about the codebase", "/ask <question>", handle_ask))
|
126
|
+
registry.register(Command("/quit", "Exit the shell", None, handle_exit))
|
127
|
+
registry.register(Command("/help", "Show help for commands", "/help [command]", handle_help))
|
128
|
+
registry.register(Command("/include", "Set paths to include in analysis", "/include <path1> [path2 ...]", handle_include, get_path_completer()))
|
129
|
+
registry.register(Command("/rinclude", "Set paths to include recursively", "/rinclude <path1> [path2 ...]", handle_rinclude, get_path_completer(True)))
|
189
130
|
|
190
131
|
# Register aliases
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
132
|
+
registry.register_alias("clear", "/clear")
|
133
|
+
registry.register_alias("quit", "/quit")
|
134
|
+
registry.register_alias("help", "/help")
|
135
|
+
registry.register_alias("/inc", "/include")
|
136
|
+
registry.register_alias("/rinc", "/rinclude")
|
janito/shell/processor.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
"""Command processor for Janito shell."""
|
2
2
|
from typing import Optional
|
3
3
|
from rich.console import Console
|
4
|
-
from .
|
4
|
+
from .registry import CommandRegistry
|
5
5
|
|
6
6
|
class CommandProcessor:
|
7
7
|
"""Processes shell commands."""
|
8
8
|
|
9
|
-
def __init__(self):
|
9
|
+
def __init__(self, registry: CommandRegistry) -> None:
|
10
|
+
"""Initialize command processor with registry."""
|
11
|
+
super().__init__()
|
10
12
|
self.console = Console()
|
11
|
-
self.
|
12
|
-
register_commands()
|
13
|
+
self.registry = registry
|
13
14
|
|
14
15
|
def process_command(self, command_line: str) -> None:
|
15
16
|
"""Process a command line input."""
|
@@ -21,32 +22,11 @@ class CommandProcessor:
|
|
21
22
|
cmd = parts[0].lower()
|
22
23
|
args = parts[1] if len(parts) > 1 else ""
|
23
24
|
|
24
|
-
|
25
|
-
if command := system.get_command(cmd):
|
26
|
-
# Special handling for /rinc command to support interactive completion
|
27
|
-
if cmd in ["/rinc", "/rinclude"] and args:
|
28
|
-
from prompt_toolkit.completion import PathCompleter
|
29
|
-
from prompt_toolkit.document import Document
|
30
|
-
|
31
|
-
completer = PathCompleter(only_directories=True)
|
32
|
-
doc = Document(args)
|
33
|
-
completions = list(completer.get_completions(doc, None))
|
34
|
-
|
35
|
-
if completions:
|
36
|
-
if len(completions) == 1:
|
37
|
-
# Single completion - use it
|
38
|
-
args = completions[0].text
|
39
|
-
else:
|
40
|
-
# Show available completions
|
41
|
-
self.console.print("\nAvailable directories:")
|
42
|
-
for comp in completions:
|
43
|
-
self.console.print(f" {comp.text}")
|
44
|
-
return
|
45
|
-
|
25
|
+
if command := self.registry.get_command(cmd):
|
46
26
|
command.handler(args)
|
47
27
|
else:
|
48
28
|
# Treat as request command
|
49
|
-
if request_cmd :=
|
29
|
+
if request_cmd := self.registry.get_command("/request"):
|
50
30
|
request_cmd.handler(command_line)
|
51
31
|
else:
|
52
32
|
self.console.print("[red]Error: Request command not registered[/red]")
|
janito/shell/prompt.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Prompt creation and configuration for Janito shell."""
|
2
|
+
from typing import Dict, Any
|
3
|
+
from pathlib import Path
|
4
|
+
from prompt_toolkit import PromptSession
|
5
|
+
from prompt_toolkit.history import FileHistory
|
6
|
+
from prompt_toolkit.completion import NestedCompleter
|
7
|
+
from rich import print as rich_print
|
8
|
+
from janito.config import config
|
9
|
+
from .registry import CommandRegistry
|
10
|
+
|
11
|
+
def create_shell_completer(registry: CommandRegistry):
|
12
|
+
"""Create command completer for shell with nested completions."""
|
13
|
+
if config.debug:
|
14
|
+
rich_print("[yellow]Creating shell completer...[/yellow]")
|
15
|
+
|
16
|
+
commands = registry.get_commands()
|
17
|
+
|
18
|
+
if config.debug:
|
19
|
+
rich_print(f"[yellow]Found {len(commands)} commands for completion[/yellow]")
|
20
|
+
|
21
|
+
# Create nested completions for commands
|
22
|
+
completions: Dict[str, Any] = {}
|
23
|
+
|
24
|
+
for cmd_name, cmd in commands.items():
|
25
|
+
if config.debug:
|
26
|
+
rich_print(f"[yellow]Setting up completion for command: {cmd_name}[/yellow]")
|
27
|
+
completions[cmd_name] = cmd.completer
|
28
|
+
|
29
|
+
if config.debug:
|
30
|
+
rich_print("[yellow]Creating nested completer from completions dictionary[/yellow]")
|
31
|
+
return NestedCompleter.from_nested_dict(completions)
|
32
|
+
|
33
|
+
def create_shell_session(registry: CommandRegistry) -> PromptSession:
|
34
|
+
"""Create and configure the shell prompt session."""
|
35
|
+
if config.debug:
|
36
|
+
rich_print("[yellow]Creating shell session...[/yellow]")
|
37
|
+
|
38
|
+
history_file = Path.home() / ".janito_history"
|
39
|
+
if config.debug:
|
40
|
+
rich_print(f"[yellow]Using history file: {history_file}[/yellow]")
|
41
|
+
|
42
|
+
completer = create_shell_completer(registry)
|
43
|
+
|
44
|
+
return PromptSession(
|
45
|
+
history=FileHistory(str(history_file)),
|
46
|
+
completer=completer,
|
47
|
+
complete_while_typing=True
|
48
|
+
)
|