From 6978d909137b1a8b78b787803e1e25f735a04424 Mon Sep 17 00:00:00 2001 From: Aaron Weisberg Date: Sun, 5 Jan 2025 19:04:20 +0100 Subject: [PATCH] refactor(comments): extract comment processing to new class Extract the comment processing logic from FileWatcher into a new dedicated CommentProcessor class to improve code organization and reusability. This allows comment processing to be used independently of file watching. --- aider/commands.py | 22 +++++++ aider/comment_processor.py | 125 +++++++++++++++++++++++++++++++++++++ aider/watch.py | 106 ++----------------------------- 3 files changed, 151 insertions(+), 102 deletions(-) create mode 100644 aider/comment_processor.py diff --git a/aider/commands.py b/aider/commands.py index 34b3881bbe7..2af9b39a2ff 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -22,6 +22,7 @@ from aider.run_cmd import run_cmd from aider.scrape import Scraper, install_playwright from aider.utils import is_image_file +from aider.comment_processor import CommentProcessor from .dump import dump # noqa: F401 @@ -681,6 +682,12 @@ def completions_add(self): files = [self.quote_fname(fn) for fn in files] return files + def completions_comments(self): + files = set(self.coder.get_all_relative_files()) + files = files - set(self.coder.get_inchat_relative_files()) + files = [self.quote_fname(fn) for fn in files] + return files + def glob_filtered_to_repo(self, pattern): if not pattern.strip(): return [] @@ -806,6 +813,21 @@ def cmd_add(self, args): self.io.tool_output(f"Added {fname} to the chat") self.coder.check_added_files() + def cmd_comments(self, args): + files = parse_quoted_filenames(args) + comment_processor = CommentProcessor(self.io, self.coder) + comment_prompt = comment_processor.process_changes(files) + + from aider.coders.base_coder import Coder + + coder = Coder.create( + io=self.io, + from_coder=self.coder, + edit_format=self.coder.edit_format, + summarize_from_coder=False, + ) + coder.run(comment_prompt) + def completions_drop(self): files = self.coder.get_inchat_relative_files() read_only_files = [self.coder.get_rel_fname(fn) for fn in self.coder.abs_read_only_fnames] diff --git a/aider/comment_processor.py b/aider/comment_processor.py new file mode 100644 index 00000000000..c4e6d714b48 --- /dev/null +++ b/aider/comment_processor.py @@ -0,0 +1,125 @@ +import re +from typing import Optional, Tuple, List + +from grep_ast import TreeContext + +from aider.io import InputOutput + + +class CommentProcessor: + """Processes AI comments in source files""" + + # Compiled regex pattern for AI comments + ai_comment_pattern = re.compile( + r"(?:#|//|--) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE + ) + + def __init__(self, io: InputOutput, coder, analytics=None): + self.io = io + self.coder = coder + self.analytics = analytics + + def get_ai_comments( + self, filepath + ) -> Tuple[Optional[List[int]], Optional[List[str]], Optional[str]]: + """Extract AI comment line numbers, comments and action status from a file""" + line_nums = [] + comments = [] + has_action = None # None, "!" or "?" + content = self.io.read_text(filepath, silent=True) + if not content: + return None, None, None + + for i, line in enumerate(content.splitlines(), 1): + if match := self.ai_comment_pattern.search(line): + comment = match.group(0).strip() + if comment: + line_nums.append(i) + comments.append(comment) + comment = comment.lower() + comment = comment.lstrip("/#-") + comment = comment.strip() + if comment.startswith("ai!") or comment.endswith("ai!"): + has_action = "!" + elif comment.startswith("ai?") or comment.endswith("ai?"): + has_action = "?" + if not line_nums: + return None, None, None + return line_nums, comments, has_action + + def process_changes(self, changed_files) -> str: + """Process file changes and generate prompt from AI comments""" + from aider.watch_prompts import watch_code_prompt, watch_ask_prompt + + has_action = None + added = False + for fname in changed_files: + _, _, action = self.get_ai_comments(fname) + if action in ("!", "?"): + has_action = action + + if fname in self.coder.abs_fnames: + continue + if self.analytics: + self.analytics.event("ai-comments file-add") + self.coder.abs_fnames.add(fname) + rel_fname = self.coder.get_rel_fname(fname) + if not added: + self.io.tool_output() + added = True + self.io.tool_output(f"Added {rel_fname} to the chat") + + if not has_action: + if added: + self.io.tool_output( + "End your comment with AI! to request changes or AI? to ask questions" + ) + return "" + + if self.analytics: + self.analytics.event("ai-comments execute") + self.io.tool_output("Processing your request...") + + if has_action == "!": + res = watch_code_prompt + elif has_action == "?": + res = watch_ask_prompt + + # Refresh all AI comments from tracked files + for fname in self.coder.abs_fnames: + line_nums, comments, _action = self.get_ai_comments(fname) + if not line_nums: + continue + + code = self.io.read_text(fname) + if not code: + continue + + rel_fname = self.coder.get_rel_fname(fname) + res += f"\n{rel_fname}:\n" + + # Convert comment line numbers to line indices (0-based) + lois = [ln - 1 for ln, _ in zip(line_nums, comments) if ln > 0] + + try: + context = TreeContext( + rel_fname, + code, + color=False, + line_number=False, + child_context=False, + last_line=False, + margin=0, + mark_lois=True, + loi_pad=3, + show_top_of_file_parent_scope=False, + ) + context.lines_of_interest = set() + context.add_lines_of_interest(lois) + context.add_context() + res += context.format() + except ValueError: + for ln, comment in zip(line_nums, comments): + res += f" Line {ln}: {comment}\n" + + return res diff --git a/aider/watch.py b/aider/watch.py index f1e24bcc587..40111c977ce 100644 --- a/aider/watch.py +++ b/aider/watch.py @@ -9,7 +9,7 @@ from watchfiles import watch from aider.dump import dump # noqa -from aider.watch_prompts import watch_ask_prompt, watch_code_prompt +from aider.comment_processor import CommentProcessor def load_gitignores(gitignore_paths: list[Path]) -> Optional[PathSpec]: @@ -81,6 +81,7 @@ def __init__(self, coder, gitignores=None, verbose=False, analytics=None, root=N [Path(g) for g in self.gitignores] if self.gitignores else [] ) + self.comment_processor = CommentProcessor(self.io, self.coder, self.analytics) coder.io.file_watcher = self def filter_func(self, change_type, path): @@ -103,7 +104,7 @@ def filter_func(self, change_type, path): # Check if file contains AI markers try: - comments, _, _ = self.get_ai_comments(str(path_abs)) + comments, _, _ = self.comment_processor.get_ai_comments(str(path_abs)) return bool(comments) except Exception: return @@ -143,106 +144,7 @@ def stop(self): def process_changes(self): """Get any detected file changes""" - - has_action = None - added = False - for fname in self.changed_files: - _, _, action = self.get_ai_comments(fname) - if action in ("!", "?"): - has_action = action - - if fname in self.coder.abs_fnames: - continue - if self.analytics: - self.analytics.event("ai-comments file-add") - self.coder.abs_fnames.add(fname) - rel_fname = self.coder.get_rel_fname(fname) - if not added: - self.io.tool_output() - added = True - self.io.tool_output(f"Added {rel_fname} to the chat") - - if not has_action: - if added: - self.io.tool_output( - "End your comment with AI! to request changes or AI? to ask questions" - ) - return "" - - if self.analytics: - self.analytics.event("ai-comments execute") - self.io.tool_output("Processing your request...") - - if has_action == "!": - res = watch_code_prompt - elif has_action == "?": - res = watch_ask_prompt - - # Refresh all AI comments from tracked files - for fname in self.coder.abs_fnames: - line_nums, comments, _action = self.get_ai_comments(fname) - if not line_nums: - continue - - code = self.io.read_text(fname) - if not code: - continue - - rel_fname = self.coder.get_rel_fname(fname) - res += f"\n{rel_fname}:\n" - - # Convert comment line numbers to line indices (0-based) - lois = [ln - 1 for ln, _ in zip(line_nums, comments) if ln > 0] - - try: - context = TreeContext( - rel_fname, - code, - color=False, - line_number=False, - child_context=False, - last_line=False, - margin=0, - mark_lois=True, - loi_pad=3, - show_top_of_file_parent_scope=False, - ) - context.lines_of_interest = set() - context.add_lines_of_interest(lois) - context.add_context() - res += context.format() - except ValueError: - for ln, comment in zip(line_nums, comments): - res += f" Line {ln}: {comment}\n" - - return res - - def get_ai_comments(self, filepath): - """Extract AI comment line numbers, comments and action status from a file""" - line_nums = [] - comments = [] - has_action = None # None, "!" or "?" - content = self.io.read_text(filepath, silent=True) - if not content: - return None, None, None - - for i, line in enumerate(content.splitlines(), 1): - if match := self.ai_comment_pattern.search(line): - comment = match.group(0).strip() - if comment: - line_nums.append(i) - comments.append(comment) - comment = comment.lower() - comment = comment.lstrip("/#-") - comment = comment.strip() - if comment.startswith("ai!") or comment.endswith("ai!"): - has_action = "!" - elif comment.startswith("ai?") or comment.endswith("ai?"): - has_action = "?" - if not line_nums: - return None, None, None - return line_nums, comments, has_action - + return self.comment_processor.process_changes(self.changed_files) def main(): """Example usage of the file watcher"""