| | """file_ops.py
|
| |
|
| | This module provides various file manipulation skills for the OpenHands agent.
|
| |
|
| | Functions:
|
| | - open_file(path: str, line_number: int | None = 1, context_lines: int = 100): Opens a file and optionally moves to a specific line.
|
| | - goto_line(line_number: int): Moves the window to show the specified line number.
|
| | - scroll_down(): Moves the window down by the number of lines specified in WINDOW.
|
| | - scroll_up(): Moves the window up by the number of lines specified in WINDOW.
|
| | - search_dir(search_term: str, dir_path: str = './'): Searches for a term in all files in the specified directory.
|
| | - search_file(search_term: str, file_path: str | None = None): Searches for a term in the specified file or the currently open file.
|
| | - find_file(file_name: str, dir_path: str = './'): Finds all files with the given name in the specified directory.
|
| | """
|
| |
|
| | import os
|
| |
|
| | from openhands.linter import DefaultLinter, LintResult
|
| |
|
| | CURRENT_FILE: str | None = None
|
| | CURRENT_LINE = 1
|
| | WINDOW = 100
|
| |
|
| |
|
| | MSG_FILE_UPDATED = '[File updated (edited at line {line_number}). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]'
|
| | LINTER_ERROR_MSG = '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _output_error(error_msg: str) -> bool:
|
| | print(f'ERROR: {error_msg}')
|
| | return False
|
| |
|
| |
|
| | def _is_valid_filename(file_name) -> bool:
|
| | if not file_name or not isinstance(file_name, str) or not file_name.strip():
|
| | return False
|
| | invalid_chars = '<>:"/\\|?*'
|
| | if os.name == 'nt':
|
| | invalid_chars = '<>:"/\\|?*'
|
| | elif os.name == 'posix':
|
| | invalid_chars = '\0'
|
| |
|
| | for char in invalid_chars:
|
| | if char in file_name:
|
| | return False
|
| | return True
|
| |
|
| |
|
| | def _is_valid_path(path) -> bool:
|
| | if not path or not isinstance(path, str):
|
| | return False
|
| | try:
|
| | return os.path.exists(os.path.normpath(path))
|
| | except PermissionError:
|
| | return False
|
| |
|
| |
|
| | def _create_paths(file_name) -> bool:
|
| | try:
|
| | dirname = os.path.dirname(file_name)
|
| | if dirname:
|
| | os.makedirs(dirname, exist_ok=True)
|
| | return True
|
| | except PermissionError:
|
| | return False
|
| |
|
| |
|
| | def _check_current_file(file_path: str | None = None) -> bool:
|
| | global CURRENT_FILE
|
| | if not file_path:
|
| | file_path = CURRENT_FILE
|
| | if not file_path or not os.path.isfile(file_path):
|
| | return _output_error('No file open. Use the open_file function first.')
|
| | return True
|
| |
|
| |
|
| | def _clamp(value, min_value, max_value):
|
| | return max(min_value, min(value, max_value))
|
| |
|
| |
|
| | def _lint_file(file_path: str) -> tuple[str | None, int | None]:
|
| | """Lint the file at the given path and return a tuple with a boolean indicating if there are errors,
|
| | and the line number of the first error, if any.
|
| |
|
| | Returns:
|
| | tuple[str | None, int | None]: (lint_error, first_error_line_number)
|
| | """
|
| | linter = DefaultLinter()
|
| | lint_error: list[LintResult] = linter.lint(file_path)
|
| | if not lint_error:
|
| |
|
| | return None, None
|
| | first_error_line = lint_error[0].line if len(lint_error) > 0 else None
|
| | error_text = 'ERRORS:\n' + '\n'.join(
|
| | [f'{file_path}:{err.line}:{err.column}: {err.message}' for err in lint_error]
|
| | )
|
| | return error_text, first_error_line
|
| |
|
| |
|
| | def _print_window(
|
| | file_path, targeted_line, window, return_str=False, ignore_window=False
|
| | ):
|
| | global CURRENT_LINE
|
| | _check_current_file(file_path)
|
| | with open(file_path) as file:
|
| | content = file.read()
|
| |
|
| |
|
| | if not content.endswith('\n'):
|
| | content += '\n'
|
| |
|
| | lines = content.splitlines(True)
|
| | total_lines = len(lines)
|
| |
|
| |
|
| | CURRENT_LINE = _clamp(targeted_line, 1, total_lines)
|
| | half_window = max(1, window // 2)
|
| | if ignore_window:
|
| |
|
| | start = max(1, CURRENT_LINE)
|
| | end = min(total_lines, CURRENT_LINE + window)
|
| | else:
|
| |
|
| | start = max(1, CURRENT_LINE - half_window)
|
| | end = min(total_lines, CURRENT_LINE + half_window)
|
| |
|
| |
|
| | if start == 1:
|
| | end = min(total_lines, start + window - 1)
|
| | if end == total_lines:
|
| | start = max(1, end - window + 1)
|
| |
|
| | output = ''
|
| |
|
| |
|
| | if start > 1:
|
| | output += f'({start - 1} more lines above)\n'
|
| | else:
|
| | output += '(this is the beginning of the file)\n'
|
| | for i in range(start, end + 1):
|
| | _new_line = f'{i}|{lines[i-1]}'
|
| | if not _new_line.endswith('\n'):
|
| | _new_line += '\n'
|
| | output += _new_line
|
| | if end < total_lines:
|
| | output += f'({total_lines - end} more lines below)\n'
|
| | else:
|
| | output += '(this is the end of the file)\n'
|
| | output = output.rstrip()
|
| |
|
| | if return_str:
|
| | return output
|
| | else:
|
| | print(output)
|
| |
|
| |
|
| | def _cur_file_header(current_file, total_lines) -> str:
|
| | if not current_file:
|
| | return ''
|
| | return f'[File: {os.path.abspath(current_file)} ({total_lines} lines total)]\n'
|
| |
|
| |
|
| | def open_file(
|
| | path: str, line_number: int | None = 1, context_lines: int | None = WINDOW
|
| | ) -> None:
|
| | """Opens the file at the given path in the editor. IF the file is to be edited, first use `scroll_down` repeatedly to read the full file!
|
| | If line_number is provided, the window will be moved to include that line.
|
| | It only shows the first 100 lines by default! `context_lines` is the max number of lines to be displayed, up to 100. Use `scroll_up` and `scroll_down` to view more content up or down.
|
| |
|
| | Args:
|
| | path: str: The path to the file to open, preferred absolute path.
|
| | line_number: int | None = 1: The line number to move to. Defaults to 1.
|
| | context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
|
| | """
|
| | global CURRENT_FILE, CURRENT_LINE, WINDOW
|
| |
|
| | if not os.path.isfile(path):
|
| | _output_error(f'File {path} not found.')
|
| | return
|
| |
|
| | CURRENT_FILE = os.path.abspath(path)
|
| | with open(CURRENT_FILE) as file:
|
| | total_lines = max(1, sum(1 for _ in file))
|
| |
|
| | if not isinstance(line_number, int) or line_number < 1 or line_number > total_lines:
|
| | _output_error(f'Line number must be between 1 and {total_lines}')
|
| | return
|
| | CURRENT_LINE = line_number
|
| |
|
| |
|
| | if context_lines is None or context_lines < 1:
|
| | context_lines = WINDOW
|
| |
|
| | output = _cur_file_header(CURRENT_FILE, total_lines)
|
| | output += _print_window(
|
| | CURRENT_FILE,
|
| | CURRENT_LINE,
|
| | _clamp(context_lines, 1, 100),
|
| | return_str=True,
|
| | ignore_window=False,
|
| | )
|
| | if output.strip().endswith('more lines below)'):
|
| | output += '\n[Use `scroll_down` to view the next 100 lines of the file!]'
|
| | print(output)
|
| |
|
| |
|
| | def goto_line(line_number: int) -> None:
|
| | """Moves the window to show the specified line number.
|
| |
|
| | Args:
|
| | line_number: int: The line number to move to.
|
| | """
|
| | global CURRENT_FILE, CURRENT_LINE, WINDOW
|
| | _check_current_file()
|
| |
|
| | with open(str(CURRENT_FILE)) as file:
|
| | total_lines = max(1, sum(1 for _ in file))
|
| | if not isinstance(line_number, int) or line_number < 1 or line_number > total_lines:
|
| | _output_error(f'Line number must be between 1 and {total_lines}.')
|
| | return
|
| |
|
| | CURRENT_LINE = _clamp(line_number, 1, total_lines)
|
| |
|
| | output = _cur_file_header(CURRENT_FILE, total_lines)
|
| | output += _print_window(
|
| | CURRENT_FILE, CURRENT_LINE, WINDOW, return_str=True, ignore_window=False
|
| | )
|
| | print(output)
|
| |
|
| |
|
| | def scroll_down() -> None:
|
| | """Moves the window down by 100 lines.
|
| |
|
| | Args:
|
| | None
|
| | """
|
| | global CURRENT_FILE, CURRENT_LINE, WINDOW
|
| | _check_current_file()
|
| |
|
| | with open(str(CURRENT_FILE)) as file:
|
| | total_lines = max(1, sum(1 for _ in file))
|
| | CURRENT_LINE = _clamp(CURRENT_LINE + WINDOW, 1, total_lines)
|
| | output = _cur_file_header(CURRENT_FILE, total_lines)
|
| | output += _print_window(
|
| | CURRENT_FILE, CURRENT_LINE, WINDOW, return_str=True, ignore_window=True
|
| | )
|
| | print(output)
|
| |
|
| |
|
| | def scroll_up() -> None:
|
| | """Moves the window up by 100 lines.
|
| |
|
| | Args:
|
| | None
|
| | """
|
| | global CURRENT_FILE, CURRENT_LINE, WINDOW
|
| | _check_current_file()
|
| |
|
| | with open(str(CURRENT_FILE)) as file:
|
| | total_lines = max(1, sum(1 for _ in file))
|
| | CURRENT_LINE = _clamp(CURRENT_LINE - WINDOW, 1, total_lines)
|
| | output = _cur_file_header(CURRENT_FILE, total_lines)
|
| | output += _print_window(
|
| | CURRENT_FILE, CURRENT_LINE, WINDOW, return_str=True, ignore_window=True
|
| | )
|
| | print(output)
|
| |
|
| |
|
| | class LineNumberError(Exception):
|
| | pass
|
| |
|
| |
|
| | def search_dir(search_term: str, dir_path: str = './') -> None:
|
| | """Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
|
| |
|
| | Args:
|
| | search_term: str: The term to search for.
|
| | dir_path: str: The path to the directory to search.
|
| | """
|
| | if not os.path.isdir(dir_path):
|
| | _output_error(f'Directory {dir_path} not found')
|
| | return
|
| | matches = []
|
| | for root, _, files in os.walk(dir_path):
|
| | for file in files:
|
| | if file.startswith('.'):
|
| | continue
|
| | file_path = os.path.join(root, file)
|
| | with open(file_path, 'r', errors='ignore') as f:
|
| | for line_num, line in enumerate(f, 1):
|
| | if search_term in line:
|
| | matches.append((file_path, line_num, line.strip()))
|
| |
|
| | if not matches:
|
| | print(f'No matches found for "{search_term}" in {dir_path}')
|
| | return
|
| |
|
| | num_matches = len(matches)
|
| | num_files = len(set(match[0] for match in matches))
|
| |
|
| | if num_files > 100:
|
| | print(
|
| | f'More than {num_files} files matched for "{search_term}" in {dir_path}. Please narrow your search.'
|
| | )
|
| | return
|
| |
|
| | print(f'[Found {num_matches} matches for "{search_term}" in {dir_path}]')
|
| | for file_path, line_num, line in matches:
|
| | print(f'{file_path} (Line {line_num}): {line}')
|
| | print(f'[End of matches for "{search_term}" in {dir_path}]')
|
| |
|
| |
|
| | def search_file(search_term: str, file_path: str | None = None) -> None:
|
| | """Searches for search_term in file. If file is not provided, searches in the current open file.
|
| |
|
| | Args:
|
| | search_term: str: The term to search for.
|
| | file_path: str | None: The path to the file to search.
|
| | """
|
| | global CURRENT_FILE
|
| | if file_path is None:
|
| | file_path = CURRENT_FILE
|
| | if file_path is None:
|
| | _output_error('No file specified or open. Use the open_file function first.')
|
| | return
|
| | if not os.path.isfile(file_path):
|
| | _output_error(f'File {file_path} not found.')
|
| | return
|
| |
|
| | matches = []
|
| | with open(file_path) as file:
|
| | for i, line in enumerate(file, 1):
|
| | if search_term in line:
|
| | matches.append((i, line.strip()))
|
| |
|
| | if matches:
|
| | print(f'[Found {len(matches)} matches for "{search_term}" in {file_path}]')
|
| | for match in matches:
|
| | print(f'Line {match[0]}: {match[1]}')
|
| | print(f'[End of matches for "{search_term}" in {file_path}]')
|
| | else:
|
| | print(f'[No matches found for "{search_term}" in {file_path}]')
|
| |
|
| |
|
| | def find_file(file_name: str, dir_path: str = './') -> None:
|
| | """Finds all files with the given name in the specified directory.
|
| |
|
| | Args:
|
| | file_name: str: The name of the file to find.
|
| | dir_path: str: The path to the directory to search.
|
| | """
|
| | if not os.path.isdir(dir_path):
|
| | _output_error(f'Directory {dir_path} not found')
|
| | return
|
| |
|
| | matches = []
|
| | for root, _, files in os.walk(dir_path):
|
| | for file in files:
|
| | if file_name in file:
|
| | matches.append(os.path.join(root, file))
|
| |
|
| | if matches:
|
| | print(f'[Found {len(matches)} matches for "{file_name}" in {dir_path}]')
|
| | for match in matches:
|
| | print(f'{match}')
|
| | print(f'[End of matches for "{file_name}" in {dir_path}]')
|
| | else:
|
| | print(f'[No matches found for "{file_name}" in {dir_path}]')
|
| |
|
| |
|
| | __all__ = [
|
| | 'open_file',
|
| | 'goto_line',
|
| | 'scroll_down',
|
| | 'scroll_up',
|
| | 'search_dir',
|
| | 'search_file',
|
| | 'find_file',
|
| | ]
|
| |
|