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.
Files changed (110) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +105 -17
  3. janito/agents/__init__.py +9 -9
  4. janito/agents/agent.py +10 -3
  5. janito/agents/claudeai.py +15 -34
  6. janito/agents/openai.py +5 -1
  7. janito/change/__init__.py +29 -16
  8. janito/change/__main__.py +0 -0
  9. janito/{analysis → change/analysis}/__init__.py +5 -15
  10. janito/change/analysis/__main__.py +7 -0
  11. janito/change/analysis/analyze.py +62 -0
  12. janito/change/analysis/formatting.py +78 -0
  13. janito/change/analysis/options.py +81 -0
  14. janito/{analysis → change/analysis}/prompts.py +33 -18
  15. janito/change/analysis/view/__init__.py +9 -0
  16. janito/change/analysis/view/terminal.py +181 -0
  17. janito/change/applier/__init__.py +5 -0
  18. janito/change/applier/file.py +58 -0
  19. janito/change/applier/main.py +156 -0
  20. janito/change/applier/text.py +247 -0
  21. janito/change/applier/workspace_dir.py +58 -0
  22. janito/change/core.py +124 -0
  23. janito/{changehistory.py → change/history.py} +12 -14
  24. janito/change/operations.py +7 -0
  25. janito/change/parser.py +287 -0
  26. janito/change/play.py +54 -0
  27. janito/change/preview.py +82 -0
  28. janito/change/prompts.py +121 -0
  29. janito/change/test.py +0 -0
  30. janito/change/validator.py +269 -0
  31. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  32. janito/change/viewer/content.py +66 -0
  33. janito/{changeviewer → change/viewer}/diff.py +19 -4
  34. janito/change/viewer/panels.py +533 -0
  35. janito/change/viewer/styling.py +114 -0
  36. janito/{changeviewer → change/viewer}/themes.py +3 -5
  37. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  38. janito/clear_statement_parser/examples.txt +326 -0
  39. janito/clear_statement_parser/models.py +104 -0
  40. janito/clear_statement_parser/parser.py +496 -0
  41. janito/cli/base.py +30 -0
  42. janito/cli/commands.py +75 -40
  43. janito/cli/functions.py +19 -194
  44. janito/cli/history.py +61 -0
  45. janito/common.py +65 -8
  46. janito/config.py +70 -5
  47. janito/demo/__init__.py +4 -0
  48. janito/demo/data.py +13 -0
  49. janito/demo/mock_data.py +20 -0
  50. janito/demo/operations.py +45 -0
  51. janito/demo/runner.py +59 -0
  52. janito/demo/scenarios.py +32 -0
  53. janito/prompt.py +36 -0
  54. janito/qa.py +6 -14
  55. janito/search_replace/README.md +192 -0
  56. janito/search_replace/__init__.py +7 -0
  57. janito/search_replace/__main__.py +21 -0
  58. janito/search_replace/core.py +120 -0
  59. janito/search_replace/logger.py +35 -0
  60. janito/search_replace/parser.py +52 -0
  61. janito/search_replace/play.py +61 -0
  62. janito/search_replace/replacer.py +36 -0
  63. janito/search_replace/searcher.py +411 -0
  64. janito/search_replace/strategy_result.py +10 -0
  65. janito/shell/__init__.py +38 -0
  66. janito/shell/bus.py +31 -0
  67. janito/shell/commands.py +136 -0
  68. janito/shell/history.py +20 -0
  69. janito/shell/processor.py +32 -0
  70. janito/shell/prompt.py +48 -0
  71. janito/shell/registry.py +60 -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 +6 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/show.py +141 -0
  83. janito/workspace/stats.py +43 -0
  84. janito/workspace/types.py +98 -0
  85. janito/workspace/workset.py +108 -0
  86. janito/workspace/workspace.py +114 -0
  87. janito-0.7.0.dist-info/METADATA +167 -0
  88. janito-0.7.0.dist-info/RECORD +96 -0
  89. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
  90. janito/_contextparser.py +0 -113
  91. janito/analysis/display.py +0 -149
  92. janito/analysis/options.py +0 -112
  93. janito/change/applier.py +0 -269
  94. janito/change/content.py +0 -62
  95. janito/change/indentation.py +0 -33
  96. janito/change/position.py +0 -169
  97. janito/changeviewer/panels.py +0 -268
  98. janito/changeviewer/styling.py +0 -59
  99. janito/console/__init__.py +0 -3
  100. janito/console/commands.py +0 -112
  101. janito/console/core.py +0 -62
  102. janito/console/display.py +0 -157
  103. janito/fileparser.py +0 -334
  104. janito/prompts.py +0 -81
  105. janito/scan.py +0 -176
  106. janito/tests/test_fileparser.py +0 -26
  107. janito-0.5.0.dist-info/METADATA +0 -146
  108. janito-0.5.0.dist-info/RECORD +0 -45
  109. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  110. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,269 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import Optional, Tuple, List, Set
4
+ from rich.console import Console
5
+ from rich.prompt import Confirm
6
+ from rich.panel import Panel
7
+ from rich.columns import Columns
8
+ from rich import box
9
+
10
+ from janito.common import progress_send_message
11
+ from janito.change.history import save_changes_to_history
12
+ from janito.config import config
13
+ from janito.workspace import workset # Updated import
14
+ from .viewer import preview_all_changes
15
+ from .parser import FileChange, ChangeOperation
16
+
17
+ from .analysis import analyze_request
18
+
19
+ def validate_file_operations(changes: List[FileChange], collected_files: Set[Path]) -> Tuple[bool, str]:
20
+ """Validate file operations against current filesystem state.
21
+
22
+ Validates:
23
+ - Path conflicts:
24
+ - Parent directory exists and is a directory
25
+ - No Python module name conflicts (dir vs file)
26
+ - Text modifications:
27
+ - Search content required for non-append modifications
28
+ - Replace content required for append operations
29
+
30
+ Args:
31
+ changes: List of file changes to validate
32
+ collected_files: Set of files that were found during scanning
33
+
34
+ Returns:
35
+ Tuple[bool, str]: (is_valid, error_message)
36
+ """
37
+ for change in changes:
38
+ # For modify operations, validate text changes
39
+ if change.operation == ChangeOperation.MODIFY_FILE:
40
+ for mod in change.text_changes:
41
+ if not mod.is_delete and not mod.is_append:
42
+ if not mod.search_content:
43
+ return False, f"Search content required for modification in {change.name}"
44
+
45
+ if mod.is_append and not mod.replace_content:
46
+ return False, f"Replace content required for append operation in {change.name}"
47
+
48
+ # Check for directory/file conflicts
49
+ if change.operation == ChangeOperation.CREATE_FILE:
50
+ parent = change.name.parent
51
+ if parent.exists() and not parent.is_dir():
52
+ return False, f"Cannot create file - parent path exists as file: {parent}"
53
+
54
+ # Check for Python module conflicts
55
+ if change.name.suffix == '.py':
56
+ module_dir = change.name.with_suffix('')
57
+ if module_dir.exists() and module_dir.is_dir():
58
+ return False, f"Cannot create Python file - directory with same name exists: {module_dir}"
59
+
60
+ # Basic rename validation (without existence checks)
61
+ if change.operation == ChangeOperation.RENAME_FILE:
62
+ if not change.source or not change.target:
63
+ return False, "Rename operation requires both source and target paths"
64
+
65
+ return True, ""
66
+ from pathlib import Path
67
+ from typing import Tuple, List, Set, Optional
68
+ from .parser import FileChange, ChangeOperation
69
+
70
+ def validate_python_syntax(code: str, filepath: Path | str) -> Tuple[bool, str]:
71
+ """Validate Python code syntax using ast parser.
72
+
73
+ Args:
74
+ code: Python source code to validate
75
+ filepath: Path or string of the file (used for error messages)
76
+
77
+ Returns:
78
+ Tuple of (is_valid, error_message)
79
+ - is_valid: True if syntax is valid
80
+ - error_message: Empty string if valid, error details if invalid
81
+ """
82
+ try:
83
+ ast.parse(code)
84
+ return True, ""
85
+ except SyntaxError as e:
86
+ # Get detailed error information
87
+ line_num = e.lineno if e.lineno is not None else 0
88
+ col_num = e.offset if e.offset is not None else 0
89
+ line = e.text or ""
90
+
91
+ # Build error message with line pointer
92
+ pointer = " " * (col_num - 1) + "^" if col_num > 0 else ""
93
+ error_msg = (
94
+ f"Syntax error at {filepath}:{line_num}:{col_num}\n"
95
+ f"{line}\n"
96
+ f"{pointer}\n"
97
+ f"Error: {str(e)}"
98
+ )
99
+ return False, error_msg
100
+ except Exception as e:
101
+ return False, f"Parsing error in {filepath}: {str(e)}"
102
+
103
+ def validate_change(change: FileChange) -> Tuple[bool, Optional[str]]:
104
+ """Validate a single FileChange object for structural correctness.
105
+
106
+ Validates:
107
+ - Required fields (name, operation type)
108
+ - Operation-specific requirements:
109
+ - Create/Replace: content is required
110
+ - Rename: target path is required
111
+ - Modify: at least one text change required
112
+ - Text change validations:
113
+ - Delete: search_content is required
114
+ - Replace: both search_content and replace_content required
115
+ - Prevents duplicate search patterns
116
+
117
+ Args:
118
+ change: FileChange object to validate
119
+
120
+ Returns:
121
+ Tuple[bool, Optional[str]]: (is_valid, error_message)
122
+ """
123
+ if not change.name:
124
+ return False, "File name is required"
125
+
126
+ operation = change.operation.name.title().lower()
127
+ if operation not in ['create_file', 'replace_file', 'remove_file', 'rename_file', 'modify_file', 'move_file']:
128
+ return False, f"Invalid file operation: {change.operation}"
129
+
130
+ if operation in ['rename_file', 'move_file'] and not change.target:
131
+ return False, "Target file path is required for rename/move operation"
132
+
133
+ if operation in ['create_file', 'replace_file']:
134
+ if not change.content:
135
+ return False, f"Content is required for {change.operation} operation"
136
+
137
+ if operation == 'modify_file':
138
+ if not change.text_changes:
139
+ return False, "At least one modification is required for modify operation"
140
+
141
+ # Track search texts to avoid duplicates
142
+ seen_search_texts = set()
143
+ for mod in change.text_changes:
144
+ # Validate append operations
145
+ if mod.is_append:
146
+ if not mod.replace_content:
147
+ return False, "Replace content required for append operation"
148
+ # Validate other operations
149
+ elif not mod.is_delete:
150
+ if not mod.search_content:
151
+ return False, "Search content required for non-append modification"
152
+
153
+ if mod.search_content:
154
+ seen_search_texts.add(mod.search_content)
155
+
156
+ return True, None
157
+
158
+ def validate_all_changes(changes: List[FileChange], collected_files: Set[Path]) -> Tuple[bool, Optional[str]]:
159
+ """Validates all aspects of the requested changes.
160
+
161
+ Performs complete validation in two phases:
162
+ 1. Individual change validation:
163
+ - Structure and content requirements
164
+ - Operation-specific validations
165
+ - Text modification validations
166
+ 2. Filesystem state validation:
167
+ - File existence checks
168
+ - Path conflict checks
169
+ - Python module conflict checks
170
+
171
+ Args:
172
+ changes: List of changes to validate
173
+ collected_files: Set of files found during scanning
174
+
175
+ Returns:
176
+ Tuple[bool, Optional[str]]: (is_valid, error_message)
177
+ - If valid, error_message will be None
178
+ - If invalid, error_message will describe the validation failure
179
+ """
180
+ # First validate individual changes
181
+ for change in changes:
182
+ is_valid, error = validate_change(change)
183
+ if not is_valid:
184
+ return False, f"Invalid change for {change.name}: {error}"
185
+
186
+ # Then validate file operations against filesystem
187
+ is_valid, error = validate_file_operations(changes, collected_files)
188
+ if not is_valid:
189
+ return False, error
190
+
191
+ return True, None
192
+
193
+ def validate_file_operations(changes: List[FileChange], collected_files: Set[Path]) -> Tuple[bool, str]:
194
+ """Validate file operations against current filesystem state.
195
+
196
+ Validates:
197
+ - File existence for operations that require it:
198
+ - Modify: file must exist in collected files
199
+ - Replace: file must exist in collected files
200
+ - Remove: file must exist in collected files
201
+ - File non-existence for operations that require it:
202
+ - Create: file must not exist (unless marked as new)
203
+ - Rename target: target must not exist
204
+ - Path conflicts:
205
+ - Parent directory exists and is a directory
206
+ - No Python module name conflicts (dir vs file)
207
+ - Text modifications:
208
+ - Search content required for non-append modifications
209
+ - Replace content required for append operations
210
+
211
+ Args:
212
+ changes: List of file changes to validate
213
+ collected_files: Set of files that were found during scanning, includes state metadata
214
+
215
+ Returns:
216
+ Tuple[bool, str]: (is_valid, error_message)
217
+ """
218
+ for change in changes:
219
+ # For modify operations, validate text changes
220
+ if change.operation == ChangeOperation.MODIFY_FILE:
221
+ for mod in change.text_changes:
222
+ if not mod.is_delete and not mod.is_append:
223
+ if not mod.search_content:
224
+ return False, f"Search content required for modification in {change.name}"
225
+
226
+ if mod.is_append and not mod.replace_content:
227
+ return False, f"Replace content required for append operation in {change.name}"
228
+
229
+ # Get file state if available
230
+ file_state = change.name.name.split(' (')[-1].rstrip(')') if ' (' in str(change.name) else None
231
+ is_new_file = file_state == 'new' if file_state else False
232
+
233
+ # Validate file exists for operations requiring it
234
+ if change.operation in (ChangeOperation.MODIFY_FILE, ChangeOperation.REPLACE_FILE, ChangeOperation.REMOVE_FILE):
235
+ if change.name not in collected_files and not is_new_file:
236
+ return False, f"File not found in scanned files: {change.name}"
237
+
238
+
239
+ # Validate rename/move operations
240
+ if change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
241
+ if not change.source or not change.target:
242
+ return False, "Rename/move operation requires both source and target paths"
243
+ if change.source not in collected_files and not is_new_file:
244
+ return False, f"Source file not found for rename/move: {change.source}"
245
+
246
+ return True, ""
247
+
248
+ def process_change_request(request: str) -> None:
249
+ """Process a change request by analyzing, validating and applying changes."""
250
+ # Ensure workset is refreshed before processing changes
251
+
252
+ # Analyze the request and get proposed changes
253
+ changes = analyze_request(request, workset._workspace.content)
254
+ if not changes:
255
+ return
256
+
257
+ # Collect the set of scanned files from workspace content
258
+ collected_files = {Path(line.replace('<path>', '').replace('</path>', '').strip())
259
+ for line in workset._workspace.content.split('\n')
260
+ if line.startswith('<path>')}
261
+
262
+ # Validate changes
263
+ is_valid, error = validate_all_changes(changes, collected_files)
264
+ if not is_valid:
265
+ console = Console()
266
+ console.print(f"\n[red]Error:[/red] {error}")
267
+ return
268
+
269
+ # ...rest of existing function code...
@@ -1,12 +1,11 @@
1
- from .panels import show_change_preview, preview_all_changes
2
1
  from .styling import set_theme
3
2
  from .themes import ColorTheme, ThemeType, get_theme_by_type
3
+ from .panels import preview_all_changes
4
4
 
5
5
  __all__ = [
6
- 'show_change_preview',
7
- 'preview_all_changes',
8
6
  'set_theme',
9
7
  'ColorTheme',
10
8
  'ThemeType',
11
- 'get_theme_by_type'
9
+ 'get_theme_by_type',
10
+ 'preview_all_changes'
12
11
  ]
@@ -0,0 +1,66 @@
1
+ from typing import Optional
2
+ from pathlib import Path
3
+ from rich.syntax import Syntax
4
+ from rich.panel import Panel
5
+ from rich.console import Console
6
+
7
+ def get_file_syntax(filepath: Path) -> Optional[str]:
8
+ """Get syntax lexer name based on file extension"""
9
+ ext_map = {
10
+ '.py': 'python',
11
+ '.js': 'javascript',
12
+ '.ts': 'typescript',
13
+ '.html': 'html',
14
+ '.css': 'css',
15
+ '.json': 'json',
16
+ '.md': 'markdown',
17
+ '.yaml': 'yaml',
18
+ '.yml': 'yaml',
19
+ '.sh': 'bash',
20
+ '.bash': 'bash',
21
+ '.sql': 'sql',
22
+ '.xml': 'xml',
23
+ }
24
+ return ext_map.get(filepath.suffix.lower())
25
+
26
+ def create_content_preview(filepath: Path, content: str, is_new: bool = False) -> Panel:
27
+ """Create a preview panel with syntax highlighting and metadata"""
28
+ syntax_type = get_file_syntax(filepath)
29
+ file_size = len(content.encode('utf-8'))
30
+ line_count = len(content.splitlines())
31
+
32
+ # Format file metadata
33
+ size_str = f"{file_size:,} bytes"
34
+ stats = f"[dim]{line_count:,} lines | {size_str}[/dim]"
35
+
36
+ if syntax_type:
37
+ # Use syntax highlighting for known file types
38
+ syntax = Syntax(
39
+ content,
40
+ syntax_type,
41
+ theme="monokai",
42
+ line_numbers=True,
43
+ word_wrap=True,
44
+ code_width=min(100, Console().width - 4),
45
+ tab_size=4
46
+ )
47
+ preview = syntax
48
+ file_type = f"[blue]{syntax_type}[/blue]"
49
+ else:
50
+ # Fallback to plain text for unknown types
51
+ preview = content
52
+ file_type = "[yellow]plain text[/yellow]"
53
+
54
+ # Adjust title based on whether it's a new file
55
+ title_prefix = "[green]New File[/green]: " if is_new else "Content Preview: "
56
+ title = f"{title_prefix}[green]{filepath.name}[/green] ({file_type})"
57
+
58
+ return Panel(
59
+ preview,
60
+ title=title,
61
+ title_align="left",
62
+ subtitle=stats,
63
+ subtitle_align="right",
64
+ border_style="green" if is_new else "cyan",
65
+ padding=(1, 2)
66
+ )
@@ -1,4 +1,5 @@
1
1
  from typing import List, Tuple
2
+ from difflib import SequenceMatcher
2
3
 
3
4
  def find_common_sections(search_lines: List[str], replace_lines: List[str]) -> Tuple[List[str], List[str], List[str], List[str], List[str]]:
4
5
  """Find common sections between search and replace content"""
@@ -9,20 +10,34 @@ def find_common_sections(search_lines: List[str], replace_lines: List[str]) -> T
9
10
  common_top.append(s)
10
11
  else:
11
12
  break
12
-
13
+
13
14
  # Find common lines from bottom
14
15
  search_remaining = search_lines[len(common_top):]
15
16
  replace_remaining = replace_lines[len(common_top):]
16
-
17
+
17
18
  common_bottom = []
18
19
  for s, r in zip(reversed(search_remaining), reversed(replace_remaining)):
19
20
  if s == r:
20
21
  common_bottom.insert(0, s)
21
22
  else:
22
23
  break
23
-
24
+
24
25
  # Get the unique middle sections
25
26
  search_middle = search_remaining[:-len(common_bottom)] if common_bottom else search_remaining
26
27
  replace_middle = replace_remaining[:-len(common_bottom)] if common_bottom else replace_remaining
27
-
28
+
28
29
  return common_top, search_middle, replace_middle, common_bottom, search_lines
30
+
31
+ def get_line_similarity(line1: str, line2: str) -> float:
32
+ """Calculate similarity ratio between two lines"""
33
+ return SequenceMatcher(None, line1, line2).ratio()
34
+
35
+ def find_similar_lines(deleted_lines: List[str], added_lines: List[str], similarity_threshold: float = 0.5) -> List[Tuple[int, int, float]]:
36
+ """Find similar lines between deleted and added content"""
37
+ similar_pairs = []
38
+ for i, del_line in enumerate(deleted_lines):
39
+ for j, add_line in enumerate(added_lines):
40
+ similarity = get_line_similarity(del_line, add_line)
41
+ if similarity >= similarity_threshold:
42
+ similar_pairs.append((i, j, similarity))
43
+ return similar_pairs