Skip to content

Commit 3a86df8

Browse files
committed
Improve the autocomplete for the sidekick /drop /add commands
In `aicodebot/coder.py`, a new method `filtered_file_list` has been added to filter files in a directory based on `.gitignore` and other ignore patterns. This method is used in the `SidekickCompleter` class to provide better command completion for file paths. The `SidekickCompleter` class has been moved from `aicodebot/helpers.py` to `aicodebot/coder.py` to better align with its usage. In `aicodebot/helpers.py`, the `SidekickCompleter` class has been removed as it's now in `aicodebot/coder.py`. In `tests/test_coder.py`, a new test has been added for the `filtered_file_list` method to ensure it's working as expected.
1 parent 3437bd6 commit 3a86df8

File tree

4 files changed

+55
-33
lines changed

4 files changed

+55
-33
lines changed

aicodebot/cli.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
from aicodebot import version as aicodebot_version
2-
from aicodebot.coder import CREATIVE_TEMPERATURE, DEFAULT_MAX_TOKENS, Coder
2+
from aicodebot.coder import CREATIVE_TEMPERATURE, DEFAULT_MAX_TOKENS, Coder, SidekickCompleter
33
from aicodebot.config import get_config_file, get_local_data_dir, read_config
4-
from aicodebot.helpers import (
5-
RichLiveCallbackHandler,
6-
SidekickCompleter,
7-
create_and_write_file,
8-
exec_and_get_output,
9-
logger,
10-
)
4+
from aicodebot.helpers import RichLiveCallbackHandler, create_and_write_file, exec_and_get_output, logger
115
from aicodebot.learn import load_documents_from_repo, store_documents
126
from aicodebot.prompts import DEFAULT_PERSONALITY, PERSONALITIES, generate_files_context, get_prompt
137
from datetime import datetime
@@ -494,7 +488,7 @@ def show_file_context(files):
494488
token_length = Coder.get_token_length(Path(file).read_text())
495489
console.print(f"\t{file} ({humanize.intcomma(token_length)} tokens)")
496490

497-
files = set(files) # Dedupe
491+
files = set(files) # Create a set for deduplication
498492
if files:
499493
show_file_context(files)
500494

aicodebot/coder.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from langchain.chat_models import ChatOpenAI
44
from openai.api_resources import engine
55
from pathlib import Path
6+
from prompt_toolkit.completion import Completer, Completion
67
import fnmatch, functools, openai, os, re, subprocess, tiktoken
78

89
DEFAULT_MAX_TOKENS = 512
@@ -28,6 +29,31 @@ def clone_repo(repo_url, repo_dir):
2829
logger.info(f"Cloning {repo_url} to {repo_dir}")
2930
subprocess.run(["git", "clone", repo_url, repo_dir], check=True)
3031

32+
@classmethod
33+
def filtered_file_list(cls, path, ignore_patterns=None, use_gitignore=True):
34+
"""Walk a directory and return a list of files that are not ignored."""
35+
ignore_patterns = ignore_patterns.copy() if ignore_patterns else []
36+
37+
base_path = Path(path)
38+
39+
if use_gitignore:
40+
# Note: .gitignore files can exist in sub directories as well, such as * in __pycache__ directories
41+
gitignore_file = base_path / ".gitignore"
42+
if gitignore_file.exists():
43+
with gitignore_file.open() as f:
44+
ignore_patterns.extend(line.strip() for line in f if line.strip() and not line.startswith("#"))
45+
46+
out = []
47+
if base_path.is_dir():
48+
if not any(fnmatch.fnmatch(base_path.name, pattern) for pattern in ignore_patterns):
49+
out.append(base_path)
50+
for item in base_path.iterdir():
51+
out += cls.filtered_file_list(item, ignore_patterns, use_gitignore)
52+
elif not any(fnmatch.fnmatch(base_path.name, pattern) for pattern in ignore_patterns):
53+
out.append(base_path)
54+
55+
return out
56+
3157
@classmethod
3258
def generate_directory_structure(cls, path, ignore_patterns=None, use_gitignore=True, indent=0):
3359
"""Generate a text representation of the directory structure of a path."""
@@ -246,3 +272,26 @@ def parse_github_url(repo_url):
246272

247273
owner, repo = match.groups()
248274
return owner, repo
275+
276+
277+
class SidekickCompleter(Completer):
278+
"""A custom prompt_toolkit completer for sidekick."""
279+
280+
def get_completions(self, document, complete_event):
281+
# Get the text before the cursor
282+
text = document.text_before_cursor
283+
284+
supported_commands = ["/add", "/drop", "/edit", "/files", "/quit"]
285+
286+
# If the text starts with a slash, it's a command
287+
if text.startswith("/"):
288+
for command in supported_commands:
289+
if command.startswith(text):
290+
yield Completion(command, start_position=-len(text))
291+
292+
if text.startswith(("/add ", "/drop ")):
293+
# If the text starts with /add or /drop, it's a file
294+
files = Coder.filtered_file_list(".", use_gitignore=True, ignore_patterns=[".git"])
295+
for file in files:
296+
if str(file).startswith(text.split()[-1]):
297+
yield Completion(str(file), start_position=-len(text.split()[-1]))

aicodebot/helpers.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from langchain.callbacks.base import BaseCallbackHandler
22
from loguru import logger
33
from pathlib import Path
4-
from prompt_toolkit.completion import Completer, Completion
54
from rich.markdown import Markdown
65
import os, subprocess, sys
76

@@ -32,29 +31,6 @@ def create_and_write_file(filename, text, overwrite=False):
3231
f.write(text)
3332

3433

35-
class SidekickCompleter(Completer):
36-
"""A custom prompt_toolkit completer for sidekick."""
37-
38-
def get_completions(self, document, complete_event):
39-
# Get the text before the cursor
40-
text = document.text_before_cursor
41-
42-
supported_commands = ["/add", "/drop", "/edit", "/files", "/quit"]
43-
44-
# If the text starts with a slash, it's a command
45-
if text.startswith("/"):
46-
for command in supported_commands:
47-
if command.startswith(text):
48-
yield Completion(command, start_position=-len(text))
49-
50-
if text.startswith(("/add ", "/drop ")):
51-
# If the text starts with /add or /drop, it's a file
52-
files = Path().rglob("*")
53-
for file in files:
54-
if str(file).startswith(text.split()[-1]):
55-
yield Completion(str(file), start_position=-len(text.split()[-1]))
56-
57-
5834
def exec_and_get_output(command):
5935
"""Execute a command and return its output as a string."""
6036
logger.debug(f"Executing command: {' '.join(command)}")

tests/test_coder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def test_generate_directory_structure(tmp_path):
4040
assert "- [File] file.txt" in directory_structure_no_gitignore
4141
assert "- [File] sub_file" in directory_structure_no_gitignore
4242

43+
file_list = Coder.filtered_file_list(".", use_gitignore=True, ignore_patterns=[".git"])
44+
assert len(file_list) > 10
45+
4346

4447
def test_get_token_length():
4548
text = ""

0 commit comments

Comments
 (0)