janito 0.8.0__py3-none-any.whl → 0.9.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 +5 -0
- janito/__main__.py +143 -120
- janito/callbacks.py +130 -0
- janito/cli.py +202 -0
- janito/config.py +63 -100
- janito/data/instructions.txt +6 -0
- janito/test_file.py +4 -0
- janito/token_report.py +73 -0
- janito/tools/__init__.py +10 -0
- janito/tools/decorators.py +84 -0
- janito/tools/delete_file.py +44 -0
- janito/tools/find_files.py +154 -0
- janito/tools/search_text.py +197 -0
- janito/tools/str_replace_editor/__init__.py +6 -0
- janito/tools/str_replace_editor/editor.py +43 -0
- janito/tools/str_replace_editor/handlers.py +338 -0
- janito/tools/str_replace_editor/utils.py +88 -0
- {janito-0.8.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +2 -2
- janito-0.9.0.dist-info/METADATA +9 -0
- janito-0.9.0.dist-info/RECORD +23 -0
- {janito-0.8.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
- janito-0.9.0.dist-info/entry_points.txt +2 -0
- janito-0.9.0.dist-info/top_level.txt +1 -0
- janito/agents/__init__.py +0 -22
- janito/agents/agent.py +0 -25
- janito/agents/claudeai.py +0 -41
- janito/agents/deepseekai.py +0 -47
- janito/change/applied_blocks.py +0 -34
- janito/change/applier.py +0 -167
- janito/change/edit_blocks.py +0 -148
- janito/change/finder.py +0 -72
- janito/change/request.py +0 -144
- janito/change/validator.py +0 -87
- janito/change/view/content.py +0 -63
- janito/change/view/diff.py +0 -44
- janito/change/view/panels.py +0 -201
- janito/change/view/sections.py +0 -69
- janito/change/view/styling.py +0 -140
- janito/change/view/summary.py +0 -37
- janito/change/view/themes.py +0 -62
- janito/change/view/viewer.py +0 -59
- janito/cli/__init__.py +0 -2
- janito/cli/commands.py +0 -68
- janito/cli/functions.py +0 -66
- janito/common.py +0 -133
- janito/data/change_prompt.txt +0 -81
- janito/data/system_prompt.txt +0 -3
- janito/qa.py +0 -56
- janito/version.py +0 -23
- janito/workspace/__init__.py +0 -8
- janito/workspace/analysis.py +0 -121
- janito/workspace/models.py +0 -97
- janito/workspace/show.py +0 -115
- janito/workspace/stats.py +0 -42
- janito/workspace/workset.py +0 -135
- janito/workspace/workspace.py +0 -335
- janito-0.8.0.dist-info/METADATA +0 -106
- janito-0.8.0.dist-info/RECORD +0 -40
- janito-0.8.0.dist-info/entry_points.txt +0 -2
janito/change/request.py
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
from rich.console import Console
|
2
|
-
from janito.common import progress_send_message, _get_system_prompt
|
3
|
-
from janito.workspace import workset, workspace
|
4
|
-
from janito.config import config
|
5
|
-
from pathlib import Path
|
6
|
-
import tempfile
|
7
|
-
from janito.change.applier import ChangeApplier
|
8
|
-
from janito.change.validator import Validator
|
9
|
-
from janito.change.edit_blocks import EditType, CodeChange, get_edit_blocks
|
10
|
-
from typing import List, Dict, Optional
|
11
|
-
import shutil
|
12
|
-
from rich.markdown import Markdown
|
13
|
-
import importlib.resources
|
14
|
-
from .view.viewer import ChangeViewer
|
15
|
-
|
16
|
-
def _get_change_prompt() -> str:
|
17
|
-
"""Get the change prompt from the package data or local file."""
|
18
|
-
try:
|
19
|
-
# First try to read from package data
|
20
|
-
with importlib.resources.files('janito.data').joinpath('change_prompt.txt').open('r') as f:
|
21
|
-
return f.read()
|
22
|
-
except Exception:
|
23
|
-
# Fallback to local file for development
|
24
|
-
local_path = Path(__file__).parent.parent / 'data' / 'change_prompt.txt'
|
25
|
-
if local_path.exists():
|
26
|
-
return local_path.read_text()
|
27
|
-
raise FileNotFoundError("Could not find change_prompt.txt")
|
28
|
-
|
29
|
-
def request_change(request: str) -> str:
|
30
|
-
"""Process a change request for the codebase and return the response."""
|
31
|
-
|
32
|
-
change_prompt = _get_change_prompt()
|
33
|
-
|
34
|
-
prompt = change_prompt.format(
|
35
|
-
request=request,
|
36
|
-
workset=workset.content
|
37
|
-
)
|
38
|
-
response = progress_send_message(prompt)
|
39
|
-
|
40
|
-
if response is None:
|
41
|
-
return "Sorry, the response was interrupted. Please try your request again."
|
42
|
-
|
43
|
-
|
44
|
-
# Store response in workspace directory
|
45
|
-
response_file = (config.workspace_dir or Path(".")) / '.janito_last_response.txt'
|
46
|
-
response_file.parent.mkdir(parents=True, exist_ok=True)
|
47
|
-
response_file.write_text(response, encoding='utf-8')
|
48
|
-
if config.debug:
|
49
|
-
print(f"Response saved to {response_file}")
|
50
|
-
handler = ResponseHandler(response)
|
51
|
-
handler.process()
|
52
|
-
|
53
|
-
class ResponseHandler:
|
54
|
-
|
55
|
-
def __init__(self, response: str):
|
56
|
-
self.response = response
|
57
|
-
self.console = Console()
|
58
|
-
self.edit_blocks = []
|
59
|
-
self.viewer = None
|
60
|
-
self.applied_blocks = None # Store applied blocks reference
|
61
|
-
|
62
|
-
def show_block_changes(self, block_marker: str):
|
63
|
-
"""Callback to display changes for a specific block marker"""
|
64
|
-
if block_marker and self.applied_blocks:
|
65
|
-
# Find the block with matching marker and show it
|
66
|
-
for block in self.applied_blocks.blocks:
|
67
|
-
if block.block_marker == block_marker:
|
68
|
-
self.viewer._show_block(block)
|
69
|
-
break
|
70
|
-
|
71
|
-
def process(self):
|
72
|
-
self.edit_blocks, self.annotated_response = get_edit_blocks(self.response)
|
73
|
-
|
74
|
-
# Setup preview directory and applier
|
75
|
-
preview_dir = workspace.setup_preview_directory()
|
76
|
-
applier = ChangeApplier(preview_dir)
|
77
|
-
self.viewer = ChangeViewer()
|
78
|
-
|
79
|
-
# Apply changes
|
80
|
-
for block in self.edit_blocks:
|
81
|
-
applier.add_edit(block)
|
82
|
-
applier.apply()
|
83
|
-
|
84
|
-
# Store reference to applied blocks
|
85
|
-
self.applied_blocks = applier.applied_blocks
|
86
|
-
|
87
|
-
# Split response into sections and display with changes
|
88
|
-
sections = self.annotated_response.split("[Edit Block ")
|
89
|
-
|
90
|
-
if sections:
|
91
|
-
self.console.print(Markdown(sections[0])) # Print initial text
|
92
|
-
for section in sections[1:]:
|
93
|
-
marker, text = section.split("]", 1)
|
94
|
-
# Find and show the corresponding block's changes
|
95
|
-
for block in self.applied_blocks.blocks:
|
96
|
-
if block.block_marker == marker:
|
97
|
-
self.viewer._show_block(block)
|
98
|
-
break
|
99
|
-
self.console.print(Markdown(text)) # Print text after the block
|
100
|
-
|
101
|
-
# Add horizontal ruler to separate changes from validation
|
102
|
-
self.console.rule("[bold]Validation", style="dim")
|
103
|
-
|
104
|
-
# Collect files that need validation (excluding deleted files)
|
105
|
-
files_to_validate = {edit.filename for edit in self.edit_blocks
|
106
|
-
if edit.edit_type != EditType.DELETE}
|
107
|
-
|
108
|
-
# Validate changes and run tests
|
109
|
-
validator = Validator(preview_dir)
|
110
|
-
validator.validate_files(files_to_validate)
|
111
|
-
validator.run_tests()
|
112
|
-
|
113
|
-
# Collect the list of created/modified/deleted files
|
114
|
-
created_files = [edit.filename for edit in self.edit_blocks if edit.edit_type == EditType.CREATE]
|
115
|
-
modified_files = set(edit.filename for edit in self.edit_blocks
|
116
|
-
if edit.edit_type in (EditType.EDIT, EditType.CLEAN)) # Include cleaned files
|
117
|
-
deleted_files = set(edit.filename for edit in self.edit_blocks if edit.edit_type == EditType.DELETE)
|
118
|
-
|
119
|
-
# prompt the user if we want to apply the changes
|
120
|
-
if config.auto_apply:
|
121
|
-
apply_changes = True
|
122
|
-
else:
|
123
|
-
self.console.print("\nApply changes to the workspace? [y/N] ", end="")
|
124
|
-
response = input().lower()
|
125
|
-
apply_changes = response.startswith('y')
|
126
|
-
if not apply_changes:
|
127
|
-
self.console.print("[yellow]Changes were not applied. Exiting...[/yellow]")
|
128
|
-
return
|
129
|
-
|
130
|
-
# Apply changes to workspace
|
131
|
-
workspace.apply_changes(preview_dir, created_files, modified_files, deleted_files)
|
132
|
-
|
133
|
-
def replay_saved_response():
|
134
|
-
response_file = (config.workspace_dir or Path(".")) / '.janito_last_response.txt'
|
135
|
-
print(response_file)
|
136
|
-
if not response_file.exists():
|
137
|
-
print("No saved response found")
|
138
|
-
return
|
139
|
-
|
140
|
-
with open(response_file, 'r', encoding="utf-8") as file:
|
141
|
-
response = file.read()
|
142
|
-
|
143
|
-
handler = ResponseHandler(response)
|
144
|
-
handler.process()
|
janito/change/validator.py
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Set
|
3
|
-
import ast
|
4
|
-
import yaml
|
5
|
-
import subprocess
|
6
|
-
import sys
|
7
|
-
import os
|
8
|
-
|
9
|
-
class Validator:
|
10
|
-
def __init__(self, preview_dir: Path):
|
11
|
-
self.preview_dir = preview_dir
|
12
|
-
self.validated_files: Set[Path] = set()
|
13
|
-
|
14
|
-
def validate_python_syntax(self, filepath: Path):
|
15
|
-
"""Validate Python file syntax using ast."""
|
16
|
-
try:
|
17
|
-
with open(filepath, 'r', encoding="utf-8") as file:
|
18
|
-
content = file.read()
|
19
|
-
ast.parse(content, filename=str(filepath))
|
20
|
-
except SyntaxError as e:
|
21
|
-
raise ValueError(f"Python syntax error in {filepath}: {e}")
|
22
|
-
except Exception as e:
|
23
|
-
raise ValueError(f"Error validating {filepath}: {e}")
|
24
|
-
|
25
|
-
def validate_files(self, files: Set[Path]):
|
26
|
-
"""Validate all modified files."""
|
27
|
-
for filepath in files:
|
28
|
-
full_path = self.preview_dir / filepath
|
29
|
-
if not full_path.exists():
|
30
|
-
raise ValueError(f"File not found after changes: {filepath}")
|
31
|
-
|
32
|
-
if filepath.suffix == '.py':
|
33
|
-
self.validate_python_syntax(full_path)
|
34
|
-
|
35
|
-
self.validated_files.add(filepath)
|
36
|
-
from rich import print as rprint
|
37
|
-
rprint(f"[green]✓[/green] Validated [cyan]{filepath}[/cyan]")
|
38
|
-
|
39
|
-
def run_tests(self):
|
40
|
-
"""Run tests if configured in janito.yaml."""
|
41
|
-
config_file = self.preview_dir / 'janito.yaml'
|
42
|
-
if not config_file.exists():
|
43
|
-
print("No test configuration found")
|
44
|
-
return
|
45
|
-
|
46
|
-
try:
|
47
|
-
with open(config_file) as f:
|
48
|
-
config = yaml.safe_load(f)
|
49
|
-
|
50
|
-
test_cmd = config.get('test_cmd')
|
51
|
-
if not test_cmd:
|
52
|
-
print("No test_cmd found in configuration")
|
53
|
-
return
|
54
|
-
|
55
|
-
print(f"Running test command: {test_cmd}")
|
56
|
-
|
57
|
-
# Save current directory
|
58
|
-
original_dir = Path.cwd()
|
59
|
-
try:
|
60
|
-
# Change to preview directory
|
61
|
-
os.chdir(self.preview_dir)
|
62
|
-
# Run the test command
|
63
|
-
exit_code = os.system(test_cmd)
|
64
|
-
finally:
|
65
|
-
# Restore original directory
|
66
|
-
os.chdir(original_dir)
|
67
|
-
|
68
|
-
if exit_code != 0:
|
69
|
-
raise ValueError(f"Test command failed with exit code {exit_code}")
|
70
|
-
|
71
|
-
from rich.panel import Panel
|
72
|
-
from rich import print as rprint
|
73
|
-
|
74
|
-
# Create a summary panel
|
75
|
-
validated_files_list = "\n".join([f"[cyan]• {f}[/cyan]" for f in sorted(self.validated_files)])
|
76
|
-
summary = Panel(
|
77
|
-
f"[green]✓[/green] All files validated successfully:\n\n{validated_files_list}",
|
78
|
-
title="[bold green]Validation Summary[/bold green]",
|
79
|
-
border_style="green"
|
80
|
-
)
|
81
|
-
rprint("\n" + summary + "\n")
|
82
|
-
print("Tests completed successfully")
|
83
|
-
|
84
|
-
except yaml.YAMLError as e:
|
85
|
-
raise ValueError(f"Error parsing janito.yaml: {e}")
|
86
|
-
except Exception as e:
|
87
|
-
raise ValueError(f"Error running tests: {e}")
|
janito/change/view/content.py
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
from typing import Optional
|
2
|
-
from pathlib import Path
|
3
|
-
from rich.syntax import Syntax
|
4
|
-
|
5
|
-
|
6
|
-
# Mapping of file extensions to syntax lexer names
|
7
|
-
FILE_EXTENSION_MAP = {
|
8
|
-
'.py': 'python',
|
9
|
-
'.js': 'javascript',
|
10
|
-
'.ts': 'typescript',
|
11
|
-
'.html': 'html',
|
12
|
-
'.css': 'css',
|
13
|
-
'.json': 'json',
|
14
|
-
'.md': 'markdown',
|
15
|
-
'.yaml': 'yaml',
|
16
|
-
'.yml': 'yaml',
|
17
|
-
'.sh': 'bash',
|
18
|
-
'.bash': 'bash',
|
19
|
-
'.sql': 'sql',
|
20
|
-
'.xml': 'xml',
|
21
|
-
'.cpp': 'cpp',
|
22
|
-
'.c': 'c',
|
23
|
-
'.h': 'cpp',
|
24
|
-
'.hpp': 'cpp',
|
25
|
-
'.java': 'java',
|
26
|
-
'.go': 'go',
|
27
|
-
'.rs': 'rust',
|
28
|
-
}
|
29
|
-
|
30
|
-
def get_file_syntax(filepath: Path) -> Optional[str]:
|
31
|
-
"""Get syntax lexer name based on file extension.
|
32
|
-
|
33
|
-
Args:
|
34
|
-
filepath: Path object containing the file path
|
35
|
-
|
36
|
-
Returns:
|
37
|
-
String containing the syntax lexer name or None if not found
|
38
|
-
"""
|
39
|
-
return ext_map.get(filepath.suffix.lower())
|
40
|
-
|
41
|
-
def create_content_preview(filepath: Path, content: str, is_new: bool = False) -> Syntax:
|
42
|
-
"""Create a preview with syntax highlighting using consistent styling
|
43
|
-
|
44
|
-
Args:
|
45
|
-
filepath: Path to the file being previewed
|
46
|
-
content: Content to preview
|
47
|
-
is_new: Whether this is a new file preview
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
Syntax highlighted content
|
51
|
-
"""
|
52
|
-
# Get file info
|
53
|
-
syntax_type = get_file_syntax(filepath)
|
54
|
-
|
55
|
-
# Create syntax highlighted content
|
56
|
-
return Syntax(
|
57
|
-
content,
|
58
|
-
syntax_type or "text",
|
59
|
-
theme="monokai",
|
60
|
-
line_numbers=True,
|
61
|
-
word_wrap=True,
|
62
|
-
tab_size=4
|
63
|
-
)
|
janito/change/view/diff.py
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
from typing import List, Tuple
|
2
|
-
from difflib import SequenceMatcher
|
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]]:
|
5
|
-
"""Find common sections between search and replace content"""
|
6
|
-
# Find common lines from top
|
7
|
-
common_top = []
|
8
|
-
for s, r in zip(search_lines, replace_lines):
|
9
|
-
if s == r:
|
10
|
-
common_top.append(s)
|
11
|
-
else:
|
12
|
-
break
|
13
|
-
|
14
|
-
# Find common lines from bottom
|
15
|
-
search_remaining = search_lines[len(common_top):]
|
16
|
-
replace_remaining = replace_lines[len(common_top):]
|
17
|
-
|
18
|
-
common_bottom = []
|
19
|
-
for s, r in zip(reversed(search_remaining), reversed(replace_remaining)):
|
20
|
-
if s == r:
|
21
|
-
common_bottom.insert(0, s)
|
22
|
-
else:
|
23
|
-
break
|
24
|
-
|
25
|
-
# Get the unique middle sections
|
26
|
-
search_middle = search_remaining[:-len(common_bottom)] if common_bottom else search_remaining
|
27
|
-
replace_middle = replace_remaining[:-len(common_bottom)] if common_bottom else replace_remaining
|
28
|
-
|
29
|
-
return common_top, search_middle, replace_middle, common_bottom, search_lines
|
30
|
-
|
31
|
-
|
32
|
-
def find_similar_lines(deleted_lines: List[str], added_lines: List[str], similarity_threshold: float = 0.5) -> List[Tuple[int, int, float]]:
|
33
|
-
"""Find similar lines between deleted and added content"""
|
34
|
-
similar_pairs = []
|
35
|
-
for i, del_line in enumerate(deleted_lines):
|
36
|
-
for j, add_line in enumerate(added_lines):
|
37
|
-
similarity = get_line_similarity(del_line, add_line)
|
38
|
-
if similarity >= similarity_threshold:
|
39
|
-
similar_pairs.append((i, j, similarity))
|
40
|
-
return similar_pairs
|
41
|
-
|
42
|
-
def get_line_similarity(line1: str, line2: str) -> float:
|
43
|
-
"""Calculate similarity ratio between two lines"""
|
44
|
-
return SequenceMatcher(None, line1, line2).ratio()
|
janito/change/view/panels.py
DELETED
@@ -1,201 +0,0 @@
|
|
1
|
-
from rich.console import Console
|
2
|
-
from typing import List, Tuple
|
3
|
-
from rich.panel import Panel
|
4
|
-
from rich.syntax import Syntax
|
5
|
-
from pathlib import Path
|
6
|
-
from rich.columns import Columns
|
7
|
-
from rich.text import Text
|
8
|
-
from rich.layout import Layout
|
9
|
-
from ..edit_blocks import EditType, CodeChange
|
10
|
-
from .styling import format_content
|
11
|
-
from .sections import find_modified_sections
|
12
|
-
|
13
|
-
# Constants for panel layout
|
14
|
-
PANEL_MIN_WIDTH = 40
|
15
|
-
PANEL_MAX_WIDTH = 120
|
16
|
-
PANEL_PADDING = 4
|
17
|
-
COLUMN_SPACING = 4
|
18
|
-
|
19
|
-
def create_diff_columns(
|
20
|
-
original_section: List[str],
|
21
|
-
modified_section: List[str],
|
22
|
-
filename: str,
|
23
|
-
start: int,
|
24
|
-
term_width: int,
|
25
|
-
context_lines: int = 3,
|
26
|
-
current_change: int = 1,
|
27
|
-
total_changes: int = 1,
|
28
|
-
operation: str = "Edit",
|
29
|
-
reason: str = None,
|
30
|
-
is_removal: bool = False
|
31
|
-
) -> Tuple[Text, Columns]: # Changed return type to return header and content separately
|
32
|
-
"""Create side-by-side diff view with consistent styling and context."""
|
33
|
-
# Create header with progress info and rule
|
34
|
-
header = Text()
|
35
|
-
header_text, header_style = create_progress_header(
|
36
|
-
operation=operation,
|
37
|
-
filename=filename,
|
38
|
-
current=current_change,
|
39
|
-
total=total_changes,
|
40
|
-
term_width=term_width,
|
41
|
-
reason=reason
|
42
|
-
)
|
43
|
-
|
44
|
-
header.append(header_text)
|
45
|
-
header.append("\n")
|
46
|
-
header.append("─" * term_width, style="dim")
|
47
|
-
|
48
|
-
# Find sections that have changed
|
49
|
-
sections = find_modified_sections(
|
50
|
-
original_section,
|
51
|
-
modified_section,
|
52
|
-
context_lines=context_lines
|
53
|
-
)
|
54
|
-
|
55
|
-
if not sections:
|
56
|
-
# If no differences, show full content
|
57
|
-
diff_columns = _create_single_section_columns(
|
58
|
-
original_section,
|
59
|
-
modified_section,
|
60
|
-
filename,
|
61
|
-
start,
|
62
|
-
term_width,
|
63
|
-
is_removal=is_removal
|
64
|
-
)
|
65
|
-
else:
|
66
|
-
# Create columns for each modified section
|
67
|
-
rendered_sections = []
|
68
|
-
for i, (orig, mod) in enumerate(sections):
|
69
|
-
if i > 0:
|
70
|
-
rendered_sections.append(Text("...\n", style="dim"))
|
71
|
-
|
72
|
-
section_columns = _create_single_section_columns(
|
73
|
-
orig, mod, filename, start, term_width, is_removal=is_removal
|
74
|
-
)
|
75
|
-
rendered_sections.append(section_columns)
|
76
|
-
|
77
|
-
# Create single column containing all sections
|
78
|
-
diff_columns = Columns(
|
79
|
-
rendered_sections,
|
80
|
-
equal=False,
|
81
|
-
expand=False,
|
82
|
-
padding=(0, 0)
|
83
|
-
)
|
84
|
-
|
85
|
-
return header, diff_columns
|
86
|
-
|
87
|
-
def _create_single_section_columns(
|
88
|
-
original: List[str],
|
89
|
-
modified: List[str],
|
90
|
-
filename: str,
|
91
|
-
start: int,
|
92
|
-
term_width: int,
|
93
|
-
is_removal: bool = False # Add parameter with default value
|
94
|
-
) -> Columns:
|
95
|
-
"""Create columns for a single diff section."""
|
96
|
-
left_width, right_width = calculate_panel_widths(
|
97
|
-
'\n'.join(original),
|
98
|
-
'\n'.join(modified),
|
99
|
-
term_width
|
100
|
-
)
|
101
|
-
|
102
|
-
# Format content with correct parameters
|
103
|
-
left_content = format_content(
|
104
|
-
original,
|
105
|
-
search_lines=original,
|
106
|
-
replace_lines=modified,
|
107
|
-
is_search=True, # This indicates it's the original/search content
|
108
|
-
width=left_width,
|
109
|
-
is_removal=is_removal # Pass the parameter
|
110
|
-
)
|
111
|
-
right_content = format_content(
|
112
|
-
modified,
|
113
|
-
search_lines=original,
|
114
|
-
replace_lines=modified,
|
115
|
-
is_search=False, # This indicates it's the modified/replace content
|
116
|
-
width=right_width
|
117
|
-
)
|
118
|
-
|
119
|
-
left_text = create_panel_text("Original", left_content, left_width)
|
120
|
-
right_text = create_panel_text("Modified", right_content, right_width)
|
121
|
-
|
122
|
-
# Create columns without manual padding
|
123
|
-
columns = Columns(
|
124
|
-
[
|
125
|
-
left_text,
|
126
|
-
Text(" " * COLUMN_SPACING),
|
127
|
-
right_text
|
128
|
-
],
|
129
|
-
equal=False,
|
130
|
-
expand=False,
|
131
|
-
padding=(0, 0)
|
132
|
-
)
|
133
|
-
|
134
|
-
return columns
|
135
|
-
|
136
|
-
def calculate_panel_widths(left_content: str, right_content: str, term_width: int) -> Tuple[int, int]:
|
137
|
-
"""Calculate optimal widths for side-by-side panels with overflow protection."""
|
138
|
-
available_width = term_width - PANEL_PADDING - COLUMN_SPACING
|
139
|
-
|
140
|
-
left_max = max((len(line) for line in left_content.splitlines()), default=0)
|
141
|
-
right_max = max((len(line) for line in right_content.splitlines()), default=0)
|
142
|
-
|
143
|
-
left_max = max(PANEL_MIN_WIDTH, min(left_max, PANEL_MAX_WIDTH))
|
144
|
-
right_max = max(PANEL_MIN_WIDTH, min(right_max, PANEL_MAX_WIDTH))
|
145
|
-
|
146
|
-
if (left_max + right_max) <= available_width:
|
147
|
-
return left_max, right_max
|
148
|
-
|
149
|
-
ratio = left_max / (left_max + right_max)
|
150
|
-
left_width = min(
|
151
|
-
PANEL_MAX_WIDTH,
|
152
|
-
max(PANEL_MIN_WIDTH, int(available_width * ratio))
|
153
|
-
)
|
154
|
-
right_width = min(
|
155
|
-
PANEL_MAX_WIDTH,
|
156
|
-
max(PANEL_MIN_WIDTH, available_width - left_width)
|
157
|
-
)
|
158
|
-
|
159
|
-
return left_width, right_width
|
160
|
-
|
161
|
-
def create_panel_text(title: str, content: str, width: int) -> Text:
|
162
|
-
"""Create a text container with centered title."""
|
163
|
-
text = Text()
|
164
|
-
title_padding = (width - len(title)) // 2
|
165
|
-
color = "red" if title == "Original" else "green"
|
166
|
-
text.append(" " * title_padding + title + " " * title_padding, style=f"{color} bold")
|
167
|
-
text.append("\n")
|
168
|
-
text.append(content)
|
169
|
-
return text
|
170
|
-
|
171
|
-
def create_progress_header(operation: str, filename: str, current: int, total: int,
|
172
|
-
term_width: int, reason: str = None, style: str = "cyan") -> Tuple[Text, str]:
|
173
|
-
"""Create a header showing filename and global change counter.
|
174
|
-
|
175
|
-
Args:
|
176
|
-
operation: Type of operation being performed
|
177
|
-
filename: Name of the file being modified
|
178
|
-
current: Current global change number
|
179
|
-
total: Total number of changes
|
180
|
-
term_width: Width of the terminal
|
181
|
-
reason: Optional reason for the change
|
182
|
-
style: Color style for the header
|
183
|
-
|
184
|
-
Returns:
|
185
|
-
Tuple of (Rich Text object, style)
|
186
|
-
"""
|
187
|
-
text = Text()
|
188
|
-
header = f"{operation}: {filename} | Progress {current}/{total}"
|
189
|
-
if reason:
|
190
|
-
header += f" | {reason}"
|
191
|
-
|
192
|
-
# Calculate padding for centering
|
193
|
-
padding = (term_width - len(header)) // 2
|
194
|
-
|
195
|
-
# Create full-width background by padding both sides
|
196
|
-
full_line = " " * padding + header + " " * (term_width - len(header) - padding)
|
197
|
-
|
198
|
-
# Apply background color to entire line with better contrast
|
199
|
-
text.append(full_line, style=f"white on dark_blue")
|
200
|
-
|
201
|
-
return text, style
|
janito/change/view/sections.py
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
from typing import List, Tuple, Set
|
2
|
-
|
3
|
-
def find_modified_sections(original: list[str], modified: list[str], context_lines: int = 3) -> list[tuple[list[str], list[str]]]:
|
4
|
-
"""
|
5
|
-
Find modified sections between original and modified text with surrounding context.
|
6
|
-
Merges sections with separator lines.
|
7
|
-
|
8
|
-
Args:
|
9
|
-
original: List of original lines
|
10
|
-
modified: List of modified linesn
|
11
|
-
context_lines: Number of unchanged context lines to include
|
12
|
-
|
13
|
-
Returns:
|
14
|
-
List of tuples containing (original_section, modified_section) line pairs
|
15
|
-
"""
|
16
|
-
# Find different lines
|
17
|
-
different_lines = get_different_lines(original, modified)
|
18
|
-
if not different_lines:
|
19
|
-
return []
|
20
|
-
|
21
|
-
return create_sections(original, modified, different_lines, context_lines)
|
22
|
-
|
23
|
-
def get_different_lines(original: List[str], modified: List[str]) -> Set[int]:
|
24
|
-
"""Find lines that differ between original and modified content"""
|
25
|
-
different_lines = set()
|
26
|
-
for i in range(max(len(original), len(modified))):
|
27
|
-
if i >= len(original) or i >= len(modified):
|
28
|
-
different_lines.add(i)
|
29
|
-
elif original[i] != modified[i]:
|
30
|
-
different_lines.add(i)
|
31
|
-
return different_lines
|
32
|
-
|
33
|
-
def create_sections(original: List[str], modified: List[str],
|
34
|
-
different_lines: Set[int], context_lines: int) -> List[Tuple[List[str], List[str]]]:
|
35
|
-
"""Create sections from different lines with context"""
|
36
|
-
current_section = set()
|
37
|
-
orig_content = []
|
38
|
-
mod_content = []
|
39
|
-
|
40
|
-
for line_num in sorted(different_lines):
|
41
|
-
if not current_section or line_num <= max(current_section) + context_lines * 2:
|
42
|
-
current_section.add(line_num)
|
43
|
-
else:
|
44
|
-
process_section(original, modified, current_section, orig_content,
|
45
|
-
mod_content, context_lines)
|
46
|
-
current_section = {line_num}
|
47
|
-
|
48
|
-
if current_section:
|
49
|
-
process_section(original, modified, current_section, orig_content,
|
50
|
-
mod_content, context_lines)
|
51
|
-
|
52
|
-
return [(orig_content, mod_content)] if orig_content else []
|
53
|
-
|
54
|
-
def process_section(original: List[str], modified: List[str],
|
55
|
-
current_section: Set[int], orig_content: List[str],
|
56
|
-
mod_content: List[str], context_lines: int) -> None:
|
57
|
-
"""Process a section and add it to the content lists"""
|
58
|
-
start = max(0, min(current_section) - context_lines)
|
59
|
-
end = min(max(len(original), len(modified)),
|
60
|
-
max(current_section) + context_lines + 1)
|
61
|
-
|
62
|
-
# Add separator if needed
|
63
|
-
if orig_content:
|
64
|
-
orig_content.append("...")
|
65
|
-
mod_content.append("...")
|
66
|
-
|
67
|
-
# Add section content
|
68
|
-
orig_content.extend(original[start:end])
|
69
|
-
mod_content.extend(modified[start:end])
|