| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- """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
- # This is also used in unit tests!
- 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': # Windows
- invalid_chars = '<>:"/\\|?*'
- elif os.name == 'posix': # Unix-like systems
- 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:
- # Linting successful. No issues found.
- 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()
- # Ensure the content ends with a newline character
- if not content.endswith('\n'):
- content += '\n'
- lines = content.splitlines(True) # Keep all line ending characters
- total_lines = len(lines)
- # cover edge cases
- CURRENT_LINE = _clamp(targeted_line, 1, total_lines)
- half_window = max(1, window // 2)
- if ignore_window:
- # Use CURRENT_LINE as starting line (for e.g. scroll_down)
- start = max(1, CURRENT_LINE)
- end = min(total_lines, CURRENT_LINE + window)
- else:
- # Ensure at least one line above and below the targeted line
- start = max(1, CURRENT_LINE - half_window)
- end = min(total_lines, CURRENT_LINE + half_window)
- # Adjust start and end to ensure at least one line above and below
- if start == 1:
- end = min(total_lines, start + window - 1)
- if end == total_lines:
- start = max(1, end - window + 1)
- output = ''
- # only display this when there's at least one line above
- 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
- # Override WINDOW with context_lines
- 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',
- ]
|