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