mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-11 07:04:59 +00:00
Merge branch 'main' into glob-add
This commit is contained in:
commit
cb2b2c5ce3
22 changed files with 465 additions and 94 deletions
|
@ -1 +1 @@
|
|||
__version__ = "0.7.2"
|
||||
__version__ = "0.8.0"
|
||||
|
|
|
@ -167,13 +167,13 @@ class Coder:
|
|||
self.find_common_root()
|
||||
|
||||
if main_model.use_repo_map and self.repo and self.gpt_prompts.repo_content_prefix:
|
||||
rm_io = io if self.verbose else None
|
||||
self.repo_map = RepoMap(
|
||||
map_tokens,
|
||||
self.root,
|
||||
self.main_model,
|
||||
rm_io,
|
||||
io,
|
||||
self.gpt_prompts.repo_content_prefix,
|
||||
self.verbose,
|
||||
)
|
||||
|
||||
if self.repo_map.use_ctags:
|
||||
|
@ -287,12 +287,21 @@ class Coder:
|
|||
]
|
||||
fence = fences[0]
|
||||
|
||||
def get_abs_fnames_content(self):
|
||||
for fname in list(self.abs_fnames):
|
||||
content = self.io.read_text(fname)
|
||||
|
||||
if content is None:
|
||||
relative_fname = self.get_rel_fname(fname)
|
||||
self.io.tool_error(f"Dropping {relative_fname} from the chat.")
|
||||
self.abs_fnames.remove(fname)
|
||||
else:
|
||||
yield fname, content
|
||||
|
||||
def choose_fence(self):
|
||||
all_content = ""
|
||||
for fname in self.abs_fnames:
|
||||
all_content += Path(fname).read_text() + "\n"
|
||||
|
||||
all_content = all_content.splitlines()
|
||||
for _fname, content in self.get_abs_fnames_content():
|
||||
all_content += content + "\n"
|
||||
|
||||
good = False
|
||||
for fence_open, fence_close in self.fences:
|
||||
|
@ -317,15 +326,15 @@ class Coder:
|
|||
fnames = self.abs_fnames
|
||||
|
||||
prompt = ""
|
||||
for fname in fnames:
|
||||
for fname, content in self.get_abs_fnames_content():
|
||||
relative_fname = self.get_rel_fname(fname)
|
||||
prompt += utils.quoted_file(fname, relative_fname, fence=self.fence)
|
||||
return prompt
|
||||
prompt = "\n"
|
||||
prompt += relative_fname
|
||||
prompt += f"\n{self.fence[0]}\n"
|
||||
prompt += content
|
||||
prompt += f"{self.fence[1]}\n"
|
||||
|
||||
def recheck_abs_fnames(self):
|
||||
self.abs_fnames = set(
|
||||
fname for fname in self.abs_fnames if Path(fname).exists() and Path(fname).is_file()
|
||||
)
|
||||
return prompt
|
||||
|
||||
def get_files_messages(self):
|
||||
all_content = ""
|
||||
|
@ -454,10 +463,6 @@ class Coder:
|
|||
]
|
||||
|
||||
messages += self.done_messages
|
||||
|
||||
# notice if files disappear
|
||||
self.recheck_abs_fnames()
|
||||
|
||||
messages += self.get_files_messages()
|
||||
messages += self.cur_messages
|
||||
|
||||
|
@ -917,8 +922,8 @@ class Coder:
|
|||
full_path = os.path.abspath(os.path.join(self.root, path))
|
||||
|
||||
if full_path in self.abs_fnames:
|
||||
if not self.dry_run and write_content:
|
||||
Path(full_path).write_text(write_content)
|
||||
if write_content:
|
||||
self.io.write_text(full_path, write_content)
|
||||
return full_path
|
||||
|
||||
if not Path(full_path).exists():
|
||||
|
@ -943,8 +948,8 @@ class Coder:
|
|||
if not self.dry_run:
|
||||
self.repo.git.add(full_path)
|
||||
|
||||
if not self.dry_run and write_content:
|
||||
Path(full_path).write_text(write_content)
|
||||
if write_content:
|
||||
self.io.write_text(full_path, write_content)
|
||||
|
||||
return full_path
|
||||
|
||||
|
|
|
@ -26,7 +26,10 @@ class EditBlockCoder(Coder):
|
|||
full_path = self.allowed_to_edit(path)
|
||||
if not full_path:
|
||||
continue
|
||||
if do_replace(full_path, original, updated, self.dry_run):
|
||||
content = self.io.read_text(full_path)
|
||||
content = do_replace(full_path, content, original, updated)
|
||||
if content:
|
||||
self.io.write_text(full_path, content)
|
||||
edited.add(path)
|
||||
continue
|
||||
self.io.tool_error(f"Failed to apply edit to {path}")
|
||||
|
@ -211,7 +214,7 @@ def strip_quoted_wrapping(res, fname=None):
|
|||
return res
|
||||
|
||||
|
||||
def do_replace(fname, before_text, after_text, dry_run=False):
|
||||
def do_replace(fname, content, before_text, after_text):
|
||||
before_text = strip_quoted_wrapping(before_text, fname)
|
||||
after_text = strip_quoted_wrapping(after_text, fname)
|
||||
fname = Path(fname)
|
||||
|
@ -219,21 +222,18 @@ def do_replace(fname, before_text, after_text, dry_run=False):
|
|||
# does it want to make a new file?
|
||||
if not fname.exists() and not before_text.strip():
|
||||
fname.touch()
|
||||
content = ""
|
||||
|
||||
content = fname.read_text()
|
||||
if content is None:
|
||||
return
|
||||
|
||||
if not before_text.strip():
|
||||
# append to existing file, or start a new file
|
||||
new_content = content + after_text
|
||||
else:
|
||||
new_content = replace_most_similar_chunk(content, before_text, after_text)
|
||||
if not new_content:
|
||||
return
|
||||
|
||||
if not dry_run:
|
||||
fname.write_text(new_content)
|
||||
|
||||
return True
|
||||
return new_content
|
||||
|
||||
|
||||
ORIGINAL = "<<<<<<< ORIGINAL"
|
||||
|
|
|
@ -135,7 +135,10 @@ class EditBlockFunctionCoder(Coder):
|
|||
full_path = self.allowed_to_edit(path)
|
||||
if not full_path:
|
||||
continue
|
||||
if do_replace(full_path, original, updated, self.dry_run):
|
||||
content = self.io.read_text(full_path)
|
||||
content = do_replace(full_path, content, original, updated)
|
||||
if content:
|
||||
self.io.write_text(full_path, content)
|
||||
edited.add(path)
|
||||
continue
|
||||
self.io.tool_error(f"Failed to apply edit to {path}")
|
||||
|
|
|
@ -90,8 +90,11 @@ class SingleWholeFileFunctionCoder(Coder):
|
|||
# ending an existing block
|
||||
full_path = os.path.abspath(os.path.join(self.root, fname))
|
||||
|
||||
with open(full_path, "r") as f:
|
||||
orig_lines = f.readlines()
|
||||
content = self.io.read_text(full_path)
|
||||
if content is None:
|
||||
orig_lines = []
|
||||
else:
|
||||
orig_lines = content.splitlines()
|
||||
|
||||
show_diff = diffs.diff_partial_update(
|
||||
orig_lines,
|
||||
|
|
|
@ -53,7 +53,7 @@ class WholeFileCoder(Coder):
|
|||
full_path = (Path(self.root) / fname).absolute()
|
||||
|
||||
if mode == "diff" and full_path.exists():
|
||||
orig_lines = full_path.read_text().splitlines(keepends=True)
|
||||
orig_lines = self.io.read_text(full_path).splitlines(keepends=True)
|
||||
|
||||
show_diff = diffs.diff_partial_update(
|
||||
orig_lines,
|
||||
|
@ -64,9 +64,8 @@ class WholeFileCoder(Coder):
|
|||
else:
|
||||
if self.allowed_to_edit(fname):
|
||||
edited.add(fname)
|
||||
if not self.dry_run:
|
||||
new_lines = "".join(new_lines)
|
||||
full_path.write_text(new_lines)
|
||||
new_lines = "".join(new_lines)
|
||||
self.io.write_text(full_path, new_lines)
|
||||
|
||||
fname = None
|
||||
new_lines = []
|
||||
|
@ -109,7 +108,7 @@ class WholeFileCoder(Coder):
|
|||
full_path = (Path(self.root) / fname).absolute()
|
||||
|
||||
if mode == "diff" and full_path.exists():
|
||||
orig_lines = full_path.read_text().splitlines(keepends=True)
|
||||
orig_lines = self.io.read_text(full_path).splitlines(keepends=True)
|
||||
|
||||
show_diff = diffs.diff_partial_update(
|
||||
orig_lines,
|
||||
|
@ -123,8 +122,7 @@ class WholeFileCoder(Coder):
|
|||
full_path = self.allowed_to_edit(fname)
|
||||
if full_path:
|
||||
edited.add(fname)
|
||||
if not self.dry_run:
|
||||
new_lines = "".join(new_lines)
|
||||
Path(full_path).write_text(new_lines)
|
||||
new_lines = "".join(new_lines)
|
||||
self.io.write_text(full_path, new_lines)
|
||||
|
||||
return edited
|
||||
|
|
|
@ -101,8 +101,11 @@ class WholeFileFunctionCoder(Coder):
|
|||
# ending an existing block
|
||||
full_path = os.path.abspath(os.path.join(self.root, fname))
|
||||
|
||||
with open(full_path, "r") as f:
|
||||
orig_lines = f.readlines()
|
||||
content = self.io.read_text(full_path)
|
||||
if content is None:
|
||||
orig_lines = []
|
||||
else:
|
||||
orig_lines = content.splitlines()
|
||||
|
||||
show_diff = diffs.diff_partial_update(
|
||||
orig_lines,
|
||||
|
|
|
@ -4,12 +4,13 @@ import os
|
|||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import git
|
||||
import tiktoken
|
||||
from prompt_toolkit.completion import Completion
|
||||
|
||||
from aider import prompts, utils
|
||||
from aider import prompts
|
||||
|
||||
|
||||
class Commands:
|
||||
|
@ -117,8 +118,10 @@ class Commands:
|
|||
# files
|
||||
for fname in self.coder.abs_fnames:
|
||||
relative_fname = self.coder.get_rel_fname(fname)
|
||||
quoted = utils.quoted_file(fname, relative_fname)
|
||||
tokens = len(self.tokenizer.encode(quoted))
|
||||
content = self.io.read_text(fname)
|
||||
# approximate
|
||||
content = f"{relative_fname}\n```\n" + content + "```\n"
|
||||
tokens = len(self.tokenizer.encode(content))
|
||||
res.append((tokens, f"{relative_fname}", "use /drop to drop from chat"))
|
||||
|
||||
self.io.tool_output("Approximate context window usage, in tokens:")
|
||||
|
@ -231,8 +234,8 @@ class Commands:
|
|||
if self.coder.repo is not None:
|
||||
create_file = self.io.confirm_ask(
|
||||
(
|
||||
f"No files matched '{word}'. Do you want to create the file and add it"
|
||||
" to git?"
|
||||
f"No files matched '{word}'. Do you want to create the file and add"
|
||||
" it to git?"
|
||||
),
|
||||
)
|
||||
else:
|
||||
|
@ -241,8 +244,7 @@ class Commands:
|
|||
)
|
||||
|
||||
if create_file:
|
||||
with open(os.path.join(self.coder.root, word), "w"):
|
||||
pass
|
||||
(Path(self.coder.root) / word).touch()
|
||||
matched_files = [word]
|
||||
if self.coder.repo is not None:
|
||||
self.coder.repo.git.add(os.path.join(self.coder.root, word))
|
||||
|
@ -254,9 +256,11 @@ class Commands:
|
|||
for matched_file in matched_files:
|
||||
abs_file_path = os.path.abspath(os.path.join(self.coder.root, matched_file))
|
||||
if abs_file_path not in self.coder.abs_fnames:
|
||||
self.coder.abs_fnames.add(abs_file_path)
|
||||
self.io.tool_output(f"Added {matched_file} to the chat")
|
||||
added_fnames.append(matched_file)
|
||||
content = self.io.read_text(abs_file_path)
|
||||
if content is not None:
|
||||
self.coder.abs_fnames.add(abs_file_path)
|
||||
self.io.tool_output(f"Added {matched_file} to the chat")
|
||||
added_fnames.append(matched_file)
|
||||
else:
|
||||
self.io.tool_error(f"{matched_file} is already in the chat")
|
||||
|
||||
|
@ -339,6 +343,10 @@ class Commands:
|
|||
else:
|
||||
other_files.append(file)
|
||||
|
||||
if not chat_files and not other_files:
|
||||
self.io.tool_output("\nNo files in chat or git repo.")
|
||||
return
|
||||
|
||||
if chat_files:
|
||||
self.io.tool_output("Files in chat:\n")
|
||||
for file in chat_files:
|
||||
|
|
|
@ -11,10 +11,10 @@ def main():
|
|||
|
||||
file_orig, file_updated = sys.argv[1], sys.argv[2]
|
||||
|
||||
with open(file_orig, "r") as f:
|
||||
with open(file_orig, "r", encoding="utf-8") as f:
|
||||
lines_orig = f.readlines()
|
||||
|
||||
with open(file_updated, "r") as f:
|
||||
with open(file_updated, "r", encoding="utf-8") as f:
|
||||
lines_updated = f.readlines()
|
||||
|
||||
for i in range(len(file_updated)):
|
||||
|
|
22
aider/io.py
22
aider/io.py
|
@ -100,6 +100,8 @@ class InputOutput:
|
|||
user_input_color="blue",
|
||||
tool_output_color=None,
|
||||
tool_error_color="red",
|
||||
encoding="utf-8",
|
||||
dry_run=False,
|
||||
):
|
||||
no_color = os.environ.get("NO_COLOR")
|
||||
if no_color is not None and no_color != "":
|
||||
|
@ -124,6 +126,9 @@ class InputOutput:
|
|||
else:
|
||||
self.chat_history_file = None
|
||||
|
||||
self.encoding = encoding
|
||||
self.dry_run = dry_run
|
||||
|
||||
if pretty:
|
||||
self.console = Console()
|
||||
else:
|
||||
|
@ -132,6 +137,23 @@ class InputOutput:
|
|||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.append_chat_history(f"\n# aider chat started at {current_time}\n\n")
|
||||
|
||||
def read_text(self, filename):
|
||||
try:
|
||||
with open(str(filename), "r", encoding=self.encoding) as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
self.tool_error(f"{filename}: file not found error")
|
||||
return
|
||||
except UnicodeError as e:
|
||||
self.tool_error(f"{filename}: {e}")
|
||||
return
|
||||
|
||||
def write_text(self, filename, content):
|
||||
if self.dry_run:
|
||||
return
|
||||
with open(str(filename), "w", encoding=self.encoding) as f:
|
||||
f.write(content)
|
||||
|
||||
def get_input(self, root, rel_fnames, addable_rel_fnames, commands):
|
||||
if self.pretty:
|
||||
style = dict(style=self.user_input_color) if self.user_input_color else dict()
|
||||
|
|
|
@ -185,6 +185,11 @@ def main(args=None, input=None, output=None):
|
|||
dest="dirty_commits",
|
||||
help="Disable commits when repo is found dirty",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--encoding",
|
||||
default="utf-8",
|
||||
help="Specify the encoding to use when reading files (default: utf-8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--openai-api-key",
|
||||
metavar="OPENAI_API_KEY",
|
||||
|
@ -247,6 +252,7 @@ def main(args=None, input=None, output=None):
|
|||
user_input_color=args.user_input_color,
|
||||
tool_output_color=args.tool_output_color,
|
||||
tool_error_color=args.tool_error_color,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
|
@ -294,8 +300,9 @@ def main(args=None, input=None, output=None):
|
|||
coder.commit(ask=True, which="repo_files")
|
||||
|
||||
if args.apply:
|
||||
with open(args.apply, "r") as f:
|
||||
content = f.read()
|
||||
content = io.read_text(args.apply)
|
||||
if content is None:
|
||||
return
|
||||
coder.apply_updates(content)
|
||||
return
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ class Model:
|
|||
if tokens == 8:
|
||||
self.prompt_price = 0.03
|
||||
self.completion_price = 0.06
|
||||
self.always_available = True
|
||||
elif tokens == 32:
|
||||
self.prompt_price = 0.06
|
||||
self.completion_price = 0.12
|
||||
|
|
|
@ -74,8 +74,10 @@ class RepoMap:
|
|||
main_model=models.GPT4,
|
||||
io=None,
|
||||
repo_content_prefix=None,
|
||||
verbose=False,
|
||||
):
|
||||
self.io = io
|
||||
self.verbose = verbose
|
||||
|
||||
if not root:
|
||||
root = os.getcwd()
|
||||
|
@ -130,7 +132,7 @@ class RepoMap:
|
|||
files_listing = self.get_ranked_tags_map(chat_files, other_files)
|
||||
if files_listing:
|
||||
num_tokens = self.token_count(files_listing)
|
||||
if self.io:
|
||||
if self.verbose:
|
||||
self.io.tool_output(f"ctags map: {num_tokens/1024:.1f} k-tokens")
|
||||
ctags_msg = " with selected ctags info"
|
||||
return files_listing, ctags_msg
|
||||
|
@ -138,7 +140,7 @@ class RepoMap:
|
|||
files_listing = self.get_simple_files_map(other_files)
|
||||
ctags_msg = ""
|
||||
num_tokens = self.token_count(files_listing)
|
||||
if self.io:
|
||||
if self.verbose:
|
||||
self.io.tool_output(f"simple map: {num_tokens/1024:.1f} k-tokens")
|
||||
if num_tokens < self.max_map_tokens:
|
||||
return files_listing, ctags_msg
|
||||
|
@ -198,7 +200,7 @@ class RepoMap:
|
|||
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
hello_py = os.path.join(tempdir, "hello.py")
|
||||
with open(hello_py, "w") as f:
|
||||
with open(hello_py, "w", encoding="utf-8") as f:
|
||||
f.write("def hello():\n print('Hello, world!')\n")
|
||||
self.run_ctags(hello_py)
|
||||
except FileNotFoundError:
|
||||
|
@ -237,10 +239,8 @@ class RepoMap:
|
|||
return idents
|
||||
|
||||
def get_name_identifiers_uncached(self, fname):
|
||||
try:
|
||||
with open(fname, "r") as f:
|
||||
content = f.read()
|
||||
except UnicodeDecodeError:
|
||||
content = self.io.read_text(fname)
|
||||
if content is None:
|
||||
return list()
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
from .dump import dump # noqa: F401
|
||||
|
||||
|
||||
def quoted_file(fname, display_fname, fence=("```", "```"), number=False):
|
||||
prompt = "\n"
|
||||
prompt += display_fname
|
||||
prompt += f"\n{fence[0]}\n"
|
||||
|
||||
file_content = Path(fname).read_text()
|
||||
lines = file_content.splitlines()
|
||||
for i, line in enumerate(lines, start=1):
|
||||
if number:
|
||||
prompt += f"{i:4d} "
|
||||
prompt += line + "\n"
|
||||
|
||||
prompt += f"{fence[1]}\n"
|
||||
return prompt
|
||||
|
||||
|
||||
def show_messages(messages, title=None, functions=None):
|
||||
if title:
|
||||
print(title.upper(), "*" * 50)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue