From d8e9da35d65cd668532b4d70a5f4788854e9571b Mon Sep 17 00:00:00 2001 From: Chad Phillips Date: Sun, 3 Nov 2024 20:17:23 -0500 Subject: [PATCH 1/2] add /editor command Opens an editor for constructing a user prompt, using the currently defined chat mode. The editor is determined as follows: Look for the following environment variables, in order: 1. AIDER_EDITOR 2. VISUAL 3. EDITOR If none of these are defined, use the following defaults: Windows: notepad OS X: vim *nix: vi If an editor is not found, a RuntimeError is raised. Any arguments passed after the /editor command are inserted as content. The temporary file used for editing has an .md extension, which can be leveraged for syntax highlighting. NOTE: The editor used MUST block the process until the editor is closed -- the default editors all do this. --- aider/commands.py | 7 +++ aider/editor.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++ aider/io.py | 15 +++-- 3 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 aider/editor.py diff --git a/aider/commands.py b/aider/commands.py index 45d19c1b8..b11fdf2c4 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1357,6 +1357,13 @@ class Commands: report_github_issue(issue_text, title=title, confirm=False) + def cmd_editor(self, initial_content=""): + "Open an editor to write a prompt" + from aider.editor import pipe_editor + user_input = pipe_editor(initial_content, suffix="md") + self.io.display_user_input(user_input) + self._generic_chat_command(user_input, self.coder.edit_format) + def expand_subdir(file_path): if file_path.is_file(): diff --git a/aider/editor.py b/aider/editor.py new file mode 100644 index 000000000..a6cbc0522 --- /dev/null +++ b/aider/editor.py @@ -0,0 +1,146 @@ +""" +Editor module for handling system text editor interactions. + +This module provides functionality to: +- Discover and launch the system's configured text editor +- Create and manage temporary files for editing +- Handle editor preferences from environment variables +- Support cross-platform editor operations +""" + +import os +import tempfile +import subprocess +import platform +import shlex +from rich.console import Console + +SYSTEM = platform.system() + +DEFAULT_EDITOR_NIX = "vi" +DEFAULT_EDITOR_OS_X = "vim" +DEFAULT_EDITOR_WINDOWS = "notepad" + +console = Console() + + +def print_status_message(success: bool, message: str, style: str | None = None) -> None: + """ + Print a status message with appropriate styling. + + :param success: Whether the operation was successful + :param message: The message to display + :param style: Optional style override. If None, uses green for success and red for failure + """ + if style is None: + style = "bold green" if success else "bold red" + console.print(message, style=style) + print("") + + +def write_temp_file(input_data: str = "", suffix: str | None = None, prefix: str | None = None, dir: str | None = None) -> str: + """ + Create a temporary file with the given input data. + + :param input_data: Content to write to the temporary file + :param suffix: Optional file extension (without the dot) + :param prefix: Optional prefix for the temporary filename + :param dir: Optional directory to create the file in + :return: Path to the created temporary file + :raises: OSError if file creation or writing fails + """ + kwargs = {"prefix": prefix, "dir": dir} + if suffix: + kwargs["suffix"] = f".{suffix}" + fd, filepath = tempfile.mkstemp(**kwargs) + try: + with os.fdopen(fd, 'w') as f: + f.write(input_data) + except Exception: + os.close(fd) + raise + return filepath + + +def get_environment_editor(default: str | None = None) -> str | None: + """ + Fetches the preferred editor from the environment variables. + + This function checks the following environment variables in order to + determine the user's preferred editor: + + - AIDER_EDITOR + - VISUAL + - EDITOR + + :param default: The default editor to return if no environment variable is set. + :type default: str or None + :return: The preferred editor as specified by environment variables or the default value. + :rtype: str or None + """ + editor = os.environ.get("AIDER_EDITOR", os.environ.get("VISUAL", os.environ.get("EDITOR", default))) + return editor + + +def discover_editor() -> list[str]: + """ + Discovers and returns the appropriate editor command as a list of arguments. + + Handles cases where the editor command includes arguments, including quoted arguments + with spaces (e.g. 'vim -c "set noswapfile"'). + + :return: A list of command parts ready for subprocess execution + :rtype: list[str] + """ + if SYSTEM == "Windows": + default_editor = DEFAULT_EDITOR_WINDOWS + elif SYSTEM == "Darwin": + default_editor = DEFAULT_EDITOR_OS_X + else: + default_editor = DEFAULT_EDITOR_NIX + editor = get_environment_editor(default_editor) + try: + return shlex.split(editor) + except ValueError as e: + raise RuntimeError(f"Invalid editor command format '{editor}': {e}") + + +def file_editor(filepath: str) -> None: + """ + Open the specified file in the system's configured editor. + + This function blocks until the editor is closed. + + :param filepath: Path to the file to edit + :type filepath: str + :raises RuntimeError: If the editor command is invalid + """ + command_parts = discover_editor() + command_parts.append(filepath) + subprocess.call(command_parts) + + +def pipe_editor(input_data: str = "", suffix: str | None = None) -> str: + """ + Opens the system editor with optional input data and returns the edited content. + + This function creates a temporary file with the provided input data, opens it in + the system editor, waits for the user to make changes and close the editor, then + reads and returns the modified content. The temporary file is deleted afterwards. + + :param input_data: Initial content to populate the editor with + :type input_data: str + :param suffix: Optional file extension for the temporary file (e.g. '.txt', '.md') + :type suffix: str or None + :return: The edited content after the editor is closed + :rtype: str + """ + filepath = write_temp_file(input_data, suffix) + file_editor(filepath) + with open(filepath, "r") as f: + output_data = f.read() + try: + os.remove(filepath) + except PermissionError: + print_status_message(False, f"WARNING: Unable to delete temporary file {filepath!r}. You may need to delete it manually.") + return output_data diff --git a/aider/io.py b/aider/io.py index 5e27a267a..cbde836a5 100644 --- a/aider/io.py +++ b/aider/io.py @@ -457,14 +457,17 @@ class InputOutput: log_file.write(f"{role.upper()} {timestamp}\n") log_file.write(content + "\n") + def display_user_input(self, inp): + if self.pretty and self.user_input_color: + style = dict(style=self.user_input_color) + else: + style = dict() + + self.console.print(Text(inp), **style) + def user_input(self, inp, log_only=True): if not log_only: - if self.pretty and self.user_input_color: - style = dict(style=self.user_input_color) - else: - style = dict() - - self.console.print(Text(inp), **style) + self.display_user_input(inp) prefix = "####" if inp: From 8801fda972e45d26e0d3b004e7aa9dee67cb57a4 Mon Sep 17 00:00:00 2001 From: Chad Phillips Date: Mon, 4 Nov 2024 10:10:37 -0600 Subject: [PATCH 2/2] formatting fixes --- aider/commands.py | 1 + aider/editor.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index b11fdf2c4..0c76e5fd4 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1360,6 +1360,7 @@ class Commands: def cmd_editor(self, initial_content=""): "Open an editor to write a prompt" from aider.editor import pipe_editor + user_input = pipe_editor(initial_content, suffix="md") self.io.display_user_input(user_input) self._generic_chat_command(user_input, self.coder.edit_format) diff --git a/aider/editor.py b/aider/editor.py index a6cbc0522..fbed5ee4f 100644 --- a/aider/editor.py +++ b/aider/editor.py @@ -9,10 +9,11 @@ This module provides functionality to: """ import os -import tempfile -import subprocess import platform import shlex +import subprocess +import tempfile + from rich.console import Console SYSTEM = platform.system() @@ -24,7 +25,7 @@ DEFAULT_EDITOR_WINDOWS = "notepad" console = Console() -def print_status_message(success: bool, message: str, style: str | None = None) -> None: +def print_status_message(success, message, style=None): """ Print a status message with appropriate styling. @@ -38,7 +39,12 @@ def print_status_message(success: bool, message: str, style: str | None = None) print("") -def write_temp_file(input_data: str = "", suffix: str | None = None, prefix: str | None = None, dir: str | None = None) -> str: +def write_temp_file( + input_data="", + suffix=None, + prefix=None, + dir=None, +): """ Create a temporary file with the given input data. @@ -54,7 +60,7 @@ def write_temp_file(input_data: str = "", suffix: str | None = None, prefix: str kwargs["suffix"] = f".{suffix}" fd, filepath = tempfile.mkstemp(**kwargs) try: - with os.fdopen(fd, 'w') as f: + with os.fdopen(fd, "w") as f: f.write(input_data) except Exception: os.close(fd) @@ -62,7 +68,7 @@ def write_temp_file(input_data: str = "", suffix: str | None = None, prefix: str return filepath -def get_environment_editor(default: str | None = None) -> str | None: +def get_environment_editor(default=None): """ Fetches the preferred editor from the environment variables. @@ -78,11 +84,13 @@ def get_environment_editor(default: str | None = None) -> str | None: :return: The preferred editor as specified by environment variables or the default value. :rtype: str or None """ - editor = os.environ.get("AIDER_EDITOR", os.environ.get("VISUAL", os.environ.get("EDITOR", default))) + editor = os.environ.get( + "AIDER_EDITOR", os.environ.get("VISUAL", os.environ.get("EDITOR", default)) + ) return editor -def discover_editor() -> list[str]: +def discover_editor(): """ Discovers and returns the appropriate editor command as a list of arguments. @@ -105,7 +113,7 @@ def discover_editor() -> list[str]: raise RuntimeError(f"Invalid editor command format '{editor}': {e}") -def file_editor(filepath: str) -> None: +def file_editor(filepath): """ Open the specified file in the system's configured editor. @@ -120,7 +128,7 @@ def file_editor(filepath: str) -> None: subprocess.call(command_parts) -def pipe_editor(input_data: str = "", suffix: str | None = None) -> str: +def pipe_editor(input_data="", suffix=None): """ Opens the system editor with optional input data and returns the edited content. @@ -142,5 +150,11 @@ def pipe_editor(input_data: str = "", suffix: str | None = None) -> str: try: os.remove(filepath) except PermissionError: - print_status_message(False, f"WARNING: Unable to delete temporary file {filepath!r}. You may need to delete it manually.") + print_status_message( + False, + ( + f"WARNING: Unable to delete temporary file {filepath!r}. You may need to delete it" + " manually." + ), + ) return output_data