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.
Files changed (59) hide show
  1. janito/__init__.py +5 -0
  2. janito/__main__.py +143 -120
  3. janito/callbacks.py +130 -0
  4. janito/cli.py +202 -0
  5. janito/config.py +63 -100
  6. janito/data/instructions.txt +6 -0
  7. janito/test_file.py +4 -0
  8. janito/token_report.py +73 -0
  9. janito/tools/__init__.py +10 -0
  10. janito/tools/decorators.py +84 -0
  11. janito/tools/delete_file.py +44 -0
  12. janito/tools/find_files.py +154 -0
  13. janito/tools/search_text.py +197 -0
  14. janito/tools/str_replace_editor/__init__.py +6 -0
  15. janito/tools/str_replace_editor/editor.py +43 -0
  16. janito/tools/str_replace_editor/handlers.py +338 -0
  17. janito/tools/str_replace_editor/utils.py +88 -0
  18. {janito-0.8.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +2 -2
  19. janito-0.9.0.dist-info/METADATA +9 -0
  20. janito-0.9.0.dist-info/RECORD +23 -0
  21. {janito-0.8.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
  22. janito-0.9.0.dist-info/entry_points.txt +2 -0
  23. janito-0.9.0.dist-info/top_level.txt +1 -0
  24. janito/agents/__init__.py +0 -22
  25. janito/agents/agent.py +0 -25
  26. janito/agents/claudeai.py +0 -41
  27. janito/agents/deepseekai.py +0 -47
  28. janito/change/applied_blocks.py +0 -34
  29. janito/change/applier.py +0 -167
  30. janito/change/edit_blocks.py +0 -148
  31. janito/change/finder.py +0 -72
  32. janito/change/request.py +0 -144
  33. janito/change/validator.py +0 -87
  34. janito/change/view/content.py +0 -63
  35. janito/change/view/diff.py +0 -44
  36. janito/change/view/panels.py +0 -201
  37. janito/change/view/sections.py +0 -69
  38. janito/change/view/styling.py +0 -140
  39. janito/change/view/summary.py +0 -37
  40. janito/change/view/themes.py +0 -62
  41. janito/change/view/viewer.py +0 -59
  42. janito/cli/__init__.py +0 -2
  43. janito/cli/commands.py +0 -68
  44. janito/cli/functions.py +0 -66
  45. janito/common.py +0 -133
  46. janito/data/change_prompt.txt +0 -81
  47. janito/data/system_prompt.txt +0 -3
  48. janito/qa.py +0 -56
  49. janito/version.py +0 -23
  50. janito/workspace/__init__.py +0 -8
  51. janito/workspace/analysis.py +0 -121
  52. janito/workspace/models.py +0 -97
  53. janito/workspace/show.py +0 -115
  54. janito/workspace/stats.py +0 -42
  55. janito/workspace/workset.py +0 -135
  56. janito/workspace/workspace.py +0 -335
  57. janito-0.8.0.dist-info/METADATA +0 -106
  58. janito-0.8.0.dist-info/RECORD +0 -40
  59. 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()
@@ -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}")
@@ -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
- )
@@ -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()
@@ -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
@@ -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])