chatmcp-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +21 -0
- aider/analytics.py +250 -0
- aider/args.py +926 -0
- aider/args_formatter.py +228 -0
- aider/coders/__init__.py +34 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +2483 -0
- aider/coders/base_prompts.py +60 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +174 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +8 -0
- aider/coders/editor_editblock_prompts.py +18 -0
- aider/coders/editor_whole_coder.py +8 -0
- aider/coders/editor_whole_prompts.py +10 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +161 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +115 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +67 -0
- aider/commands.py +1665 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +107 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/history.py +143 -0
- aider/io.py +1175 -0
- aider/linter.py +304 -0
- aider/llm.py +47 -0
- aider/main.py +1267 -0
- aider/mdstream.py +243 -0
- aider/models.py +1286 -0
- aider/onboarding.py +428 -0
- aider/openrouter.py +128 -0
- aider/prompts.py +64 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +23 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +623 -0
- aider/repomap.py +847 -0
- aider/report.py +200 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +468 -0
- aider/resources/model-settings.yml +1767 -0
- aider/run_cmd.py +132 -0
- aider/scrape.py +284 -0
- aider/sendchat.py +61 -0
- aider/special.py +203 -0
- aider/urls.py +17 -0
- aider/utils.py +338 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +187 -0
- aider/waiting.py +221 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +1881 -0
- aider/website/docs/config/aider_conf.md +527 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +478 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/model-aliases.md +103 -0
- aider/website/docs/config/options.md +843 -0
- aider/website/docs/config/reasoning.md +209 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +378 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +105 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +103 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +122 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +137 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +132 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +92 -0
- aider/website/share/index.md +101 -0
- chatmcp_cli-0.1.0.dist-info/METADATA +502 -0
- chatmcp_cli-0.1.0.dist-info/RECORD +228 -0
- chatmcp_cli-0.1.0.dist-info/WHEEL +5 -0
- chatmcp_cli-0.1.0.dist-info/entry_points.txt +3 -0
- chatmcp_cli-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
- chatmcp_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,657 @@
|
|
1
|
+
import difflib
|
2
|
+
import math
|
3
|
+
import re
|
4
|
+
import sys
|
5
|
+
from difflib import SequenceMatcher
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
from aider import utils
|
9
|
+
|
10
|
+
from ..dump import dump # noqa: F401
|
11
|
+
from .base_coder import Coder
|
12
|
+
from .editblock_prompts import EditBlockPrompts
|
13
|
+
|
14
|
+
|
15
|
+
class EditBlockCoder(Coder):
|
16
|
+
"""A coder that uses search/replace blocks for code modifications."""
|
17
|
+
|
18
|
+
edit_format = "diff"
|
19
|
+
gpt_prompts = EditBlockPrompts()
|
20
|
+
|
21
|
+
def get_edits(self):
|
22
|
+
content = self.partial_response_content
|
23
|
+
|
24
|
+
# might raise ValueError for malformed ORIG/UPD blocks
|
25
|
+
edits = list(
|
26
|
+
find_original_update_blocks(
|
27
|
+
content,
|
28
|
+
self.fence,
|
29
|
+
self.get_inchat_relative_files(),
|
30
|
+
)
|
31
|
+
)
|
32
|
+
|
33
|
+
self.shell_commands += [edit[1] for edit in edits if edit[0] is None]
|
34
|
+
edits = [edit for edit in edits if edit[0] is not None]
|
35
|
+
|
36
|
+
return edits
|
37
|
+
|
38
|
+
def apply_edits_dry_run(self, edits):
|
39
|
+
return self.apply_edits(edits, dry_run=True)
|
40
|
+
|
41
|
+
def apply_edits(self, edits, dry_run=False):
|
42
|
+
failed = []
|
43
|
+
passed = []
|
44
|
+
updated_edits = []
|
45
|
+
|
46
|
+
for edit in edits:
|
47
|
+
path, original, updated = edit
|
48
|
+
full_path = self.abs_root_path(path)
|
49
|
+
new_content = None
|
50
|
+
|
51
|
+
if Path(full_path).exists():
|
52
|
+
content = self.io.read_text(full_path)
|
53
|
+
new_content = do_replace(full_path, content, original, updated, self.fence)
|
54
|
+
|
55
|
+
# If the edit failed, and
|
56
|
+
# this is not a "create a new file" with an empty original...
|
57
|
+
# https://github.com/Aider-AI/aider/issues/2258
|
58
|
+
if not new_content and original.strip():
|
59
|
+
# try patching any of the other files in the chat
|
60
|
+
for full_path in self.abs_fnames:
|
61
|
+
content = self.io.read_text(full_path)
|
62
|
+
new_content = do_replace(full_path, content, original, updated, self.fence)
|
63
|
+
if new_content:
|
64
|
+
path = self.get_rel_fname(full_path)
|
65
|
+
break
|
66
|
+
|
67
|
+
updated_edits.append((path, original, updated))
|
68
|
+
|
69
|
+
if new_content:
|
70
|
+
if not dry_run:
|
71
|
+
self.io.write_text(full_path, new_content)
|
72
|
+
passed.append(edit)
|
73
|
+
else:
|
74
|
+
failed.append(edit)
|
75
|
+
|
76
|
+
if dry_run:
|
77
|
+
return updated_edits
|
78
|
+
|
79
|
+
if not failed:
|
80
|
+
return
|
81
|
+
|
82
|
+
blocks = "block" if len(failed) == 1 else "blocks"
|
83
|
+
|
84
|
+
res = f"# {len(failed)} SEARCH/REPLACE {blocks} failed to match!\n"
|
85
|
+
for edit in failed:
|
86
|
+
path, original, updated = edit
|
87
|
+
|
88
|
+
full_path = self.abs_root_path(path)
|
89
|
+
content = self.io.read_text(full_path)
|
90
|
+
|
91
|
+
res += f"""
|
92
|
+
## SearchReplaceNoExactMatch: This SEARCH block failed to exactly match lines in {path}
|
93
|
+
<<<<<<< SEARCH
|
94
|
+
{original}=======
|
95
|
+
{updated}>>>>>>> REPLACE
|
96
|
+
|
97
|
+
"""
|
98
|
+
did_you_mean = find_similar_lines(original, content)
|
99
|
+
if did_you_mean:
|
100
|
+
res += f"""Did you mean to match some of these actual lines from {path}?
|
101
|
+
|
102
|
+
{self.fence[0]}
|
103
|
+
{did_you_mean}
|
104
|
+
{self.fence[1]}
|
105
|
+
|
106
|
+
"""
|
107
|
+
|
108
|
+
if updated in content and updated:
|
109
|
+
res += f"""Are you sure you need this SEARCH/REPLACE block?
|
110
|
+
The REPLACE lines are already in {path}!
|
111
|
+
|
112
|
+
"""
|
113
|
+
res += (
|
114
|
+
"The SEARCH section must exactly match an existing block of lines including all white"
|
115
|
+
" space, comments, indentation, docstrings, etc\n"
|
116
|
+
)
|
117
|
+
if passed:
|
118
|
+
pblocks = "block" if len(passed) == 1 else "blocks"
|
119
|
+
res += f"""
|
120
|
+
# The other {len(passed)} SEARCH/REPLACE {pblocks} were applied successfully.
|
121
|
+
Don't re-send them.
|
122
|
+
Just reply with fixed versions of the {blocks} above that failed to match.
|
123
|
+
"""
|
124
|
+
raise ValueError(res)
|
125
|
+
|
126
|
+
|
127
|
+
def prep(content):
|
128
|
+
if content and not content.endswith("\n"):
|
129
|
+
content += "\n"
|
130
|
+
lines = content.splitlines(keepends=True)
|
131
|
+
return content, lines
|
132
|
+
|
133
|
+
|
134
|
+
def perfect_or_whitespace(whole_lines, part_lines, replace_lines):
|
135
|
+
# Try for a perfect match
|
136
|
+
res = perfect_replace(whole_lines, part_lines, replace_lines)
|
137
|
+
if res:
|
138
|
+
return res
|
139
|
+
|
140
|
+
# Try being flexible about leading whitespace
|
141
|
+
res = replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines)
|
142
|
+
if res:
|
143
|
+
return res
|
144
|
+
|
145
|
+
|
146
|
+
def perfect_replace(whole_lines, part_lines, replace_lines):
|
147
|
+
part_tup = tuple(part_lines)
|
148
|
+
part_len = len(part_lines)
|
149
|
+
|
150
|
+
for i in range(len(whole_lines) - part_len + 1):
|
151
|
+
whole_tup = tuple(whole_lines[i : i + part_len])
|
152
|
+
if part_tup == whole_tup:
|
153
|
+
res = whole_lines[:i] + replace_lines + whole_lines[i + part_len :]
|
154
|
+
return "".join(res)
|
155
|
+
|
156
|
+
|
157
|
+
def replace_most_similar_chunk(whole, part, replace):
|
158
|
+
"""Best efforts to find the `part` lines in `whole` and replace them with `replace`"""
|
159
|
+
|
160
|
+
whole, whole_lines = prep(whole)
|
161
|
+
part, part_lines = prep(part)
|
162
|
+
replace, replace_lines = prep(replace)
|
163
|
+
|
164
|
+
res = perfect_or_whitespace(whole_lines, part_lines, replace_lines)
|
165
|
+
if res:
|
166
|
+
return res
|
167
|
+
|
168
|
+
# drop leading empty line, GPT sometimes adds them spuriously (issue #25)
|
169
|
+
if len(part_lines) > 2 and not part_lines[0].strip():
|
170
|
+
skip_blank_line_part_lines = part_lines[1:]
|
171
|
+
res = perfect_or_whitespace(whole_lines, skip_blank_line_part_lines, replace_lines)
|
172
|
+
if res:
|
173
|
+
return res
|
174
|
+
|
175
|
+
# Try to handle when it elides code with ...
|
176
|
+
try:
|
177
|
+
res = try_dotdotdots(whole, part, replace)
|
178
|
+
if res:
|
179
|
+
return res
|
180
|
+
except ValueError:
|
181
|
+
pass
|
182
|
+
|
183
|
+
return
|
184
|
+
# Try fuzzy matching
|
185
|
+
res = replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines)
|
186
|
+
if res:
|
187
|
+
return res
|
188
|
+
|
189
|
+
|
190
|
+
def try_dotdotdots(whole, part, replace):
|
191
|
+
"""
|
192
|
+
See if the edit block has ... lines.
|
193
|
+
If not, return none.
|
194
|
+
|
195
|
+
If yes, try and do a perfect edit with the ... chunks.
|
196
|
+
If there's a mismatch or otherwise imperfect edit, raise ValueError.
|
197
|
+
|
198
|
+
If perfect edit succeeds, return the updated whole.
|
199
|
+
"""
|
200
|
+
|
201
|
+
dots_re = re.compile(r"(^\s*\.\.\.\n)", re.MULTILINE | re.DOTALL)
|
202
|
+
|
203
|
+
part_pieces = re.split(dots_re, part)
|
204
|
+
replace_pieces = re.split(dots_re, replace)
|
205
|
+
|
206
|
+
if len(part_pieces) != len(replace_pieces):
|
207
|
+
raise ValueError("Unpaired ... in SEARCH/REPLACE block")
|
208
|
+
|
209
|
+
if len(part_pieces) == 1:
|
210
|
+
# no dots in this edit block, just return None
|
211
|
+
return
|
212
|
+
|
213
|
+
# Compare odd strings in part_pieces and replace_pieces
|
214
|
+
all_dots_match = all(part_pieces[i] == replace_pieces[i] for i in range(1, len(part_pieces), 2))
|
215
|
+
|
216
|
+
if not all_dots_match:
|
217
|
+
raise ValueError("Unmatched ... in SEARCH/REPLACE block")
|
218
|
+
|
219
|
+
part_pieces = [part_pieces[i] for i in range(0, len(part_pieces), 2)]
|
220
|
+
replace_pieces = [replace_pieces[i] for i in range(0, len(replace_pieces), 2)]
|
221
|
+
|
222
|
+
pairs = zip(part_pieces, replace_pieces)
|
223
|
+
for part, replace in pairs:
|
224
|
+
if not part and not replace:
|
225
|
+
continue
|
226
|
+
|
227
|
+
if not part and replace:
|
228
|
+
if not whole.endswith("\n"):
|
229
|
+
whole += "\n"
|
230
|
+
whole += replace
|
231
|
+
continue
|
232
|
+
|
233
|
+
if whole.count(part) == 0:
|
234
|
+
raise ValueError
|
235
|
+
if whole.count(part) > 1:
|
236
|
+
raise ValueError
|
237
|
+
|
238
|
+
whole = whole.replace(part, replace, 1)
|
239
|
+
|
240
|
+
return whole
|
241
|
+
|
242
|
+
|
243
|
+
def replace_part_with_missing_leading_whitespace(whole_lines, part_lines, replace_lines):
|
244
|
+
# GPT often messes up leading whitespace.
|
245
|
+
# It usually does it uniformly across the ORIG and UPD blocks.
|
246
|
+
# Either omitting all leading whitespace, or including only some of it.
|
247
|
+
|
248
|
+
# Outdent everything in part_lines and replace_lines by the max fixed amount possible
|
249
|
+
leading = [len(p) - len(p.lstrip()) for p in part_lines if p.strip()] + [
|
250
|
+
len(p) - len(p.lstrip()) for p in replace_lines if p.strip()
|
251
|
+
]
|
252
|
+
|
253
|
+
if leading and min(leading):
|
254
|
+
num_leading = min(leading)
|
255
|
+
part_lines = [p[num_leading:] if p.strip() else p for p in part_lines]
|
256
|
+
replace_lines = [p[num_leading:] if p.strip() else p for p in replace_lines]
|
257
|
+
|
258
|
+
# can we find an exact match not including the leading whitespace
|
259
|
+
num_part_lines = len(part_lines)
|
260
|
+
|
261
|
+
for i in range(len(whole_lines) - num_part_lines + 1):
|
262
|
+
add_leading = match_but_for_leading_whitespace(
|
263
|
+
whole_lines[i : i + num_part_lines], part_lines
|
264
|
+
)
|
265
|
+
|
266
|
+
if add_leading is None:
|
267
|
+
continue
|
268
|
+
|
269
|
+
replace_lines = [add_leading + rline if rline.strip() else rline for rline in replace_lines]
|
270
|
+
whole_lines = whole_lines[:i] + replace_lines + whole_lines[i + num_part_lines :]
|
271
|
+
return "".join(whole_lines)
|
272
|
+
|
273
|
+
return None
|
274
|
+
|
275
|
+
|
276
|
+
def match_but_for_leading_whitespace(whole_lines, part_lines):
|
277
|
+
num = len(whole_lines)
|
278
|
+
|
279
|
+
# does the non-whitespace all agree?
|
280
|
+
if not all(whole_lines[i].lstrip() == part_lines[i].lstrip() for i in range(num)):
|
281
|
+
return
|
282
|
+
|
283
|
+
# are they all offset the same?
|
284
|
+
add = set(
|
285
|
+
whole_lines[i][: len(whole_lines[i]) - len(part_lines[i])]
|
286
|
+
for i in range(num)
|
287
|
+
if whole_lines[i].strip()
|
288
|
+
)
|
289
|
+
|
290
|
+
if len(add) != 1:
|
291
|
+
return
|
292
|
+
|
293
|
+
return add.pop()
|
294
|
+
|
295
|
+
|
296
|
+
def replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines):
|
297
|
+
similarity_thresh = 0.8
|
298
|
+
|
299
|
+
max_similarity = 0
|
300
|
+
most_similar_chunk_start = -1
|
301
|
+
most_similar_chunk_end = -1
|
302
|
+
|
303
|
+
scale = 0.1
|
304
|
+
min_len = math.floor(len(part_lines) * (1 - scale))
|
305
|
+
max_len = math.ceil(len(part_lines) * (1 + scale))
|
306
|
+
|
307
|
+
for length in range(min_len, max_len):
|
308
|
+
for i in range(len(whole_lines) - length + 1):
|
309
|
+
chunk = whole_lines[i : i + length]
|
310
|
+
chunk = "".join(chunk)
|
311
|
+
|
312
|
+
similarity = SequenceMatcher(None, chunk, part).ratio()
|
313
|
+
|
314
|
+
if similarity > max_similarity and similarity:
|
315
|
+
max_similarity = similarity
|
316
|
+
most_similar_chunk_start = i
|
317
|
+
most_similar_chunk_end = i + length
|
318
|
+
|
319
|
+
if max_similarity < similarity_thresh:
|
320
|
+
return
|
321
|
+
|
322
|
+
modified_whole = (
|
323
|
+
whole_lines[:most_similar_chunk_start]
|
324
|
+
+ replace_lines
|
325
|
+
+ whole_lines[most_similar_chunk_end:]
|
326
|
+
)
|
327
|
+
modified_whole = "".join(modified_whole)
|
328
|
+
|
329
|
+
return modified_whole
|
330
|
+
|
331
|
+
|
332
|
+
DEFAULT_FENCE = ("`" * 3, "`" * 3)
|
333
|
+
|
334
|
+
|
335
|
+
def strip_quoted_wrapping(res, fname=None, fence=DEFAULT_FENCE):
|
336
|
+
"""
|
337
|
+
Given an input string which may have extra "wrapping" around it, remove the wrapping.
|
338
|
+
For example:
|
339
|
+
|
340
|
+
filename.ext
|
341
|
+
```
|
342
|
+
We just want this content
|
343
|
+
Not the filename and triple quotes
|
344
|
+
```
|
345
|
+
"""
|
346
|
+
if not res:
|
347
|
+
return res
|
348
|
+
|
349
|
+
res = res.splitlines()
|
350
|
+
|
351
|
+
if fname and res[0].strip().endswith(Path(fname).name):
|
352
|
+
res = res[1:]
|
353
|
+
|
354
|
+
if res[0].startswith(fence[0]) and res[-1].startswith(fence[1]):
|
355
|
+
res = res[1:-1]
|
356
|
+
|
357
|
+
res = "\n".join(res)
|
358
|
+
if res and res[-1] != "\n":
|
359
|
+
res += "\n"
|
360
|
+
|
361
|
+
return res
|
362
|
+
|
363
|
+
|
364
|
+
def do_replace(fname, content, before_text, after_text, fence=None):
|
365
|
+
before_text = strip_quoted_wrapping(before_text, fname, fence)
|
366
|
+
after_text = strip_quoted_wrapping(after_text, fname, fence)
|
367
|
+
fname = Path(fname)
|
368
|
+
|
369
|
+
# does it want to make a new file?
|
370
|
+
if not fname.exists() and not before_text.strip():
|
371
|
+
fname.touch()
|
372
|
+
content = ""
|
373
|
+
|
374
|
+
if content is None:
|
375
|
+
return
|
376
|
+
|
377
|
+
if not before_text.strip():
|
378
|
+
# append to existing file, or start a new file
|
379
|
+
new_content = content + after_text
|
380
|
+
else:
|
381
|
+
new_content = replace_most_similar_chunk(content, before_text, after_text)
|
382
|
+
|
383
|
+
return new_content
|
384
|
+
|
385
|
+
|
386
|
+
HEAD = r"^<{5,9} SEARCH\s*$"
|
387
|
+
DIVIDER = r"^={5,9}\s*$"
|
388
|
+
UPDATED = r"^>{5,9} REPLACE\s*$"
|
389
|
+
|
390
|
+
HEAD_ERR = "<<<<<<< SEARCH"
|
391
|
+
DIVIDER_ERR = "======="
|
392
|
+
UPDATED_ERR = ">>>>>>> REPLACE"
|
393
|
+
|
394
|
+
separators = "|".join([HEAD, DIVIDER, UPDATED])
|
395
|
+
|
396
|
+
split_re = re.compile(r"^((?:" + separators + r")[ ]*\n)", re.MULTILINE | re.DOTALL)
|
397
|
+
|
398
|
+
|
399
|
+
missing_filename_err = (
|
400
|
+
"Bad/missing filename. The filename must be alone on the line before the opening fence"
|
401
|
+
" {fence[0]}"
|
402
|
+
)
|
403
|
+
|
404
|
+
# Always be willing to treat triple-backticks as a fence when searching for filenames
|
405
|
+
triple_backticks = "`" * 3
|
406
|
+
|
407
|
+
|
408
|
+
def strip_filename(filename, fence):
|
409
|
+
filename = filename.strip()
|
410
|
+
|
411
|
+
if filename == "...":
|
412
|
+
return
|
413
|
+
|
414
|
+
start_fence = fence[0]
|
415
|
+
if filename.startswith(start_fence):
|
416
|
+
candidate = filename[len(start_fence) :]
|
417
|
+
if candidate and ("." in candidate or "/" in candidate):
|
418
|
+
return candidate
|
419
|
+
return
|
420
|
+
|
421
|
+
if filename.startswith(triple_backticks):
|
422
|
+
candidate = filename[len(triple_backticks) :]
|
423
|
+
if candidate and ("." in candidate or "/" in candidate):
|
424
|
+
return candidate
|
425
|
+
return
|
426
|
+
|
427
|
+
filename = filename.rstrip(":")
|
428
|
+
filename = filename.lstrip("#")
|
429
|
+
filename = filename.strip()
|
430
|
+
filename = filename.strip("`")
|
431
|
+
filename = filename.strip("*")
|
432
|
+
|
433
|
+
# https://github.com/Aider-AI/aider/issues/1158
|
434
|
+
# filename = filename.replace("\\_", "_")
|
435
|
+
|
436
|
+
return filename
|
437
|
+
|
438
|
+
|
439
|
+
def find_original_update_blocks(content, fence=DEFAULT_FENCE, valid_fnames=None):
|
440
|
+
lines = content.splitlines(keepends=True)
|
441
|
+
i = 0
|
442
|
+
current_filename = None
|
443
|
+
|
444
|
+
head_pattern = re.compile(HEAD)
|
445
|
+
divider_pattern = re.compile(DIVIDER)
|
446
|
+
updated_pattern = re.compile(UPDATED)
|
447
|
+
|
448
|
+
while i < len(lines):
|
449
|
+
line = lines[i]
|
450
|
+
|
451
|
+
# Check for shell code blocks
|
452
|
+
shell_starts = [
|
453
|
+
"```bash",
|
454
|
+
"```sh",
|
455
|
+
"```shell",
|
456
|
+
"```cmd",
|
457
|
+
"```batch",
|
458
|
+
"```powershell",
|
459
|
+
"```ps1",
|
460
|
+
"```zsh",
|
461
|
+
"```fish",
|
462
|
+
"```ksh",
|
463
|
+
"```csh",
|
464
|
+
"```tcsh",
|
465
|
+
]
|
466
|
+
|
467
|
+
# Check if the next line or the one after that is an editblock
|
468
|
+
next_is_editblock = (
|
469
|
+
i + 1 < len(lines)
|
470
|
+
and head_pattern.match(lines[i + 1].strip())
|
471
|
+
or i + 2 < len(lines)
|
472
|
+
and head_pattern.match(lines[i + 2].strip())
|
473
|
+
)
|
474
|
+
|
475
|
+
if any(line.strip().startswith(start) for start in shell_starts) and not next_is_editblock:
|
476
|
+
shell_content = []
|
477
|
+
i += 1
|
478
|
+
while i < len(lines) and not lines[i].strip().startswith("```"):
|
479
|
+
shell_content.append(lines[i])
|
480
|
+
i += 1
|
481
|
+
if i < len(lines) and lines[i].strip().startswith("```"):
|
482
|
+
i += 1 # Skip the closing ```
|
483
|
+
|
484
|
+
yield None, "".join(shell_content)
|
485
|
+
continue
|
486
|
+
|
487
|
+
# Check for SEARCH/REPLACE blocks
|
488
|
+
if head_pattern.match(line.strip()):
|
489
|
+
try:
|
490
|
+
# if next line after HEAD exists and is DIVIDER, it's a new file
|
491
|
+
if i + 1 < len(lines) and divider_pattern.match(lines[i + 1].strip()):
|
492
|
+
filename = find_filename(lines[max(0, i - 3) : i], fence, None)
|
493
|
+
else:
|
494
|
+
filename = find_filename(lines[max(0, i - 3) : i], fence, valid_fnames)
|
495
|
+
|
496
|
+
if not filename:
|
497
|
+
if current_filename:
|
498
|
+
filename = current_filename
|
499
|
+
else:
|
500
|
+
raise ValueError(missing_filename_err.format(fence=fence))
|
501
|
+
|
502
|
+
current_filename = filename
|
503
|
+
|
504
|
+
original_text = []
|
505
|
+
i += 1
|
506
|
+
while i < len(lines) and not divider_pattern.match(lines[i].strip()):
|
507
|
+
original_text.append(lines[i])
|
508
|
+
i += 1
|
509
|
+
|
510
|
+
if i >= len(lines) or not divider_pattern.match(lines[i].strip()):
|
511
|
+
raise ValueError(f"Expected `{DIVIDER_ERR}`")
|
512
|
+
|
513
|
+
updated_text = []
|
514
|
+
i += 1
|
515
|
+
while i < len(lines) and not (
|
516
|
+
updated_pattern.match(lines[i].strip())
|
517
|
+
or divider_pattern.match(lines[i].strip())
|
518
|
+
):
|
519
|
+
updated_text.append(lines[i])
|
520
|
+
i += 1
|
521
|
+
|
522
|
+
if i >= len(lines) or not (
|
523
|
+
updated_pattern.match(lines[i].strip())
|
524
|
+
or divider_pattern.match(lines[i].strip())
|
525
|
+
):
|
526
|
+
raise ValueError(f"Expected `{UPDATED_ERR}` or `{DIVIDER_ERR}`")
|
527
|
+
|
528
|
+
yield filename, "".join(original_text), "".join(updated_text)
|
529
|
+
|
530
|
+
except ValueError as e:
|
531
|
+
processed = "".join(lines[: i + 1])
|
532
|
+
err = e.args[0]
|
533
|
+
raise ValueError(f"{processed}\n^^^ {err}")
|
534
|
+
|
535
|
+
i += 1
|
536
|
+
|
537
|
+
|
538
|
+
def find_filename(lines, fence, valid_fnames):
|
539
|
+
"""
|
540
|
+
Deepseek Coder v2 has been doing this:
|
541
|
+
|
542
|
+
|
543
|
+
```python
|
544
|
+
word_count.py
|
545
|
+
```
|
546
|
+
```python
|
547
|
+
<<<<<<< SEARCH
|
548
|
+
...
|
549
|
+
|
550
|
+
This is a more flexible search back for filenames.
|
551
|
+
"""
|
552
|
+
|
553
|
+
if valid_fnames is None:
|
554
|
+
valid_fnames = []
|
555
|
+
|
556
|
+
# Go back through the 3 preceding lines
|
557
|
+
lines.reverse()
|
558
|
+
lines = lines[:3]
|
559
|
+
|
560
|
+
filenames = []
|
561
|
+
for line in lines:
|
562
|
+
# If we find a filename, done
|
563
|
+
filename = strip_filename(line, fence)
|
564
|
+
if filename:
|
565
|
+
filenames.append(filename)
|
566
|
+
|
567
|
+
# Only continue as long as we keep seeing fences
|
568
|
+
if not line.startswith(fence[0]) and not line.startswith(triple_backticks):
|
569
|
+
break
|
570
|
+
|
571
|
+
if not filenames:
|
572
|
+
return
|
573
|
+
|
574
|
+
# pick the *best* filename found
|
575
|
+
|
576
|
+
# Check for exact match first
|
577
|
+
for fname in filenames:
|
578
|
+
if fname in valid_fnames:
|
579
|
+
return fname
|
580
|
+
|
581
|
+
# Check for partial match (basename match)
|
582
|
+
for fname in filenames:
|
583
|
+
for vfn in valid_fnames:
|
584
|
+
if fname == Path(vfn).name:
|
585
|
+
return vfn
|
586
|
+
|
587
|
+
# Perform fuzzy matching with valid_fnames
|
588
|
+
for fname in filenames:
|
589
|
+
close_matches = difflib.get_close_matches(fname, valid_fnames, n=1, cutoff=0.8)
|
590
|
+
if len(close_matches) == 1:
|
591
|
+
return close_matches[0]
|
592
|
+
|
593
|
+
# If no fuzzy match, look for a file w/extension
|
594
|
+
for fname in filenames:
|
595
|
+
if "." in fname:
|
596
|
+
return fname
|
597
|
+
|
598
|
+
if filenames:
|
599
|
+
return filenames[0]
|
600
|
+
|
601
|
+
|
602
|
+
def find_similar_lines(search_lines, content_lines, threshold=0.6):
|
603
|
+
search_lines = search_lines.splitlines()
|
604
|
+
content_lines = content_lines.splitlines()
|
605
|
+
|
606
|
+
best_ratio = 0
|
607
|
+
best_match = None
|
608
|
+
|
609
|
+
for i in range(len(content_lines) - len(search_lines) + 1):
|
610
|
+
chunk = content_lines[i : i + len(search_lines)]
|
611
|
+
ratio = SequenceMatcher(None, search_lines, chunk).ratio()
|
612
|
+
if ratio > best_ratio:
|
613
|
+
best_ratio = ratio
|
614
|
+
best_match = chunk
|
615
|
+
best_match_i = i
|
616
|
+
|
617
|
+
if best_ratio < threshold:
|
618
|
+
return ""
|
619
|
+
|
620
|
+
if best_match[0] == search_lines[0] and best_match[-1] == search_lines[-1]:
|
621
|
+
return "\n".join(best_match)
|
622
|
+
|
623
|
+
N = 5
|
624
|
+
best_match_end = min(len(content_lines), best_match_i + len(search_lines) + N)
|
625
|
+
best_match_i = max(0, best_match_i - N)
|
626
|
+
|
627
|
+
best = content_lines[best_match_i:best_match_end]
|
628
|
+
return "\n".join(best)
|
629
|
+
|
630
|
+
|
631
|
+
def main():
|
632
|
+
history_md = Path(sys.argv[1]).read_text()
|
633
|
+
if not history_md:
|
634
|
+
return
|
635
|
+
|
636
|
+
messages = utils.split_chat_history_markdown(history_md)
|
637
|
+
|
638
|
+
for msg in messages:
|
639
|
+
msg = msg["content"]
|
640
|
+
edits = list(find_original_update_blocks(msg))
|
641
|
+
|
642
|
+
for fname, before, after in edits:
|
643
|
+
# Compute diff
|
644
|
+
diff = difflib.unified_diff(
|
645
|
+
before.splitlines(keepends=True),
|
646
|
+
after.splitlines(keepends=True),
|
647
|
+
fromfile="before",
|
648
|
+
tofile="after",
|
649
|
+
)
|
650
|
+
diff = "".join(diff)
|
651
|
+
dump(before)
|
652
|
+
dump(after)
|
653
|
+
dump(diff)
|
654
|
+
|
655
|
+
|
656
|
+
if __name__ == "__main__":
|
657
|
+
main()
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from ..dump import dump # noqa: F401
|
2
|
+
from .editblock_coder import EditBlockCoder
|
3
|
+
from .editblock_fenced_prompts import EditBlockFencedPrompts
|
4
|
+
|
5
|
+
|
6
|
+
class EditBlockFencedCoder(EditBlockCoder):
|
7
|
+
"""A coder that uses fenced search/replace blocks for code modifications."""
|
8
|
+
|
9
|
+
edit_format = "diff-fenced"
|
10
|
+
gpt_prompts = EditBlockFencedPrompts()
|