Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds comment processing command #2780

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
120 changes: 120 additions & 0 deletions aider/comment_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import re
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):
"""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):
"""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 @@ -144,106 +145,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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path

from aider.io import InputOutput
from aider.watch import FileWatcher
from aider.comment_processor import CommentProcessor


def test_gitignore_patterns():
Expand Down Expand Up @@ -74,12 +74,12 @@ def get_rel_fname(self, fname):

io = InputOutput(pretty=False, fancy_input=False, yes=False)
coder = MinimalCoder(io)
watcher = FileWatcher(coder)
comment_processor = CommentProcessor(io, coder)
fixtures_dir = Path(__file__).parent.parent / "fixtures"

# Test Python fixture
py_path = fixtures_dir / "watch.py"
py_lines, py_comments, py_has_bang = watcher.get_ai_comments(str(py_path))
py_lines, py_comments, py_has_bang = comment_processor.get_ai_comments(str(py_path))

# Count unique AI comments (excluding duplicates and variations with extra spaces)
unique_py_comments = set(comment.strip().lower() for comment in py_comments)
Expand All @@ -93,7 +93,7 @@ def get_rel_fname(self, fname):

# Test JavaScript fixture
js_path = fixtures_dir / "watch.js"
js_lines, js_comments, js_has_bang = watcher.get_ai_comments(str(js_path))
js_lines, js_comments, js_has_bang = comment_processor.get_ai_comments(str(js_path))
js_expected = 16
assert (
len(js_lines) == js_expected
Expand All @@ -102,8 +102,8 @@ def get_rel_fname(self, fname):

# Test watch_question.js fixture
question_js_path = fixtures_dir / "watch_question.js"
question_js_lines, question_js_comments, question_js_has_bang = watcher.get_ai_comments(
str(question_js_path)
question_js_lines, question_js_comments, question_js_has_bang = (
comment_processor.get_ai_comments(str(question_js_path))
)
question_js_expected = 6
assert len(question_js_lines) == question_js_expected, (
Expand Down
Loading