Skip to content

Commit

Permalink
refactor(comments): extract comment processing to new class
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aweis89 committed Jan 5, 2025
1 parent 37ad475 commit 6978d90
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 102 deletions.
22 changes: 22 additions & 0 deletions aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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]
Expand Down
125 changes: 125 additions & 0 deletions aider/comment_processor.py
Original file line number Diff line number Diff line change
@@ -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
106 changes: 4 additions & 102 deletions aider/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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"""
Expand Down

0 comments on commit 6978d90

Please sign in to comment.